我在这里做错了什么?我得到4个零而不是:
2
4
6
8
我也很想修改我的.asm函数,以便通过更长的 vector 运行-因为在这里简化了,我只使用了一个具有四个元素的 vector ,这样我就可以对这个 vector 求和而不用SIMD 256位寄存器进行循环。
.cpp
#include <iostream>
#include <chrono>
extern "C" double *addVec(double *C, double *A, double *B, size_t &N);
int main()
{
size_t N = 1 << 2;
size_t reductions = N / 4;
double *A = (double*)_aligned_malloc(N*sizeof(double), 32);
double *B = (double*)_aligned_malloc(N*sizeof(double), 32);
double *C = (double*)_aligned_malloc(N*sizeof(double), 32);
for (size_t i = 0; i < N; i++)
{
A[i] = double(i + 1);
B[i] = double(i + 1);
}
auto start = std::chrono::high_resolution_clock::now();
double *out = addVec(C, A, B, reductions);
auto finish = std::chrono::high_resolution_clock::now();
for (size_t i = 0; i < N; i++)
{
std::cout << out[i] << std::endl;
}
std::cout << "\n\n";
std::cout << std::chrono::duration_cast<std::chrono::nanoseconds>(finish - start).count() << " ns\n";
std::cin.get();
_aligned_free(A);
_aligned_free(B);
_aligned_free(C);
return 0;
}
.asm
.data
; C -> RCX
; A -> RDX
; B -> r8
; N -> r9
.code
addVec proc
;xor rbx, rbx
align 16
;aIn:
vmovapd ymm0, ymmword ptr [rdx]
;vmovapd ymm1, ymmword ptr [rdx + rbx + 4]
vmovapd ymm2, ymmword ptr [r8]
;vmovapd ymm3, ymmword ptr [r8 + rbx + 4]
vaddpd ymm0, ymm2, ymm3
vmovapd ymmword ptr [rcx], ymm3
;inc rbx
;cmp rbx, qword ptr [r9]
;jl aIn
mov rax, rcx ; return the address of the output vector
ret
addVec endp
end
我也想作一些其他澄清:
#pragma openmp parallel for
for (size_t i = 0; i < reductions; i++)
addVec(C + i, A + i, B + i)
这将派生coreNumber + hyperThreading线程,并且每个线程在四个double上执行SIMD加法吗?那么,每个周期总共有4个coreNumber是两倍吗?我不能在这里添加hyperThreading吗?
更新可以执行此操作吗?:
.data
;// C -> RCX
;// A -> RDX
;// B -> r8
.code
addVec proc
; One cycle 8 micro-op
vmovapd ymm0, ymmword ptr [rdx] ; 1 port
vmovapd ymm1, ymmword ptr [rdx + 32]; 1 port
vmovapd ymm2, ymmword ptr [r8] ; 1 port
vmovapd ymm3, ymmword ptr [r8 + 32] ; 1 port
vfmadd231pd ymm0, ymm2, ymm4 ; 1 port
vfmadd231pd ymm1, ymm3, ymm4 ; 1 port
vmovapd ymmword ptr [rcx], ymm0 ; 1 port
vmovapd ymmword ptr [rcx + 32], ymm1; 1 port
; Return the address of the output vector
mov rax, rcx ; 1 port ?
ret
addVec endp
end
还是仅仅因为这会超过您告诉我的六个端口?
.data
;// C -> RCX
;// A -> RDX
;// B -> r8
.code
addVec proc
;align 16
; One cycle 5 micro-op ?
vmovapd ymm0, ymmword ptr [rdx] ; 1 port
vmovapd ymm1, ymmword ptr [r8] ; 1 port
vfmadd231pd ymm0, ymm1, ymm2 ; 1 port
vmovapd ymmword ptr [rcx], ymm0 ; 1 port
; Return the address of the output vector
mov rax, rcx ; 1 port ?
ret
addVec endp
end
最佳答案
代码得到错误结果的原因是程序集中的语法向后。
您正在使用Intel语法,其中目标应位于源之前。因此,在您原始的.asm代码中,您应该进行更改
vaddpd ymm0, ymm2, ymm3
到
vaddpd ymm3, ymm2, ymm0
一种查看方式是使用内部函数,然后查看反汇编。
extern "C" double *addVec(double * __restrict C, double * __restrict A, double * __restrict B, size_t &N) {
__m256d x = _mm256_load_pd((const double*)A);
__m256d y = _mm256_load_pd((const double*)B);
__m256d z = _mm256_add_pd(x,y);
_mm256_store_pd((double*)C, z);
return C;
}
在Linux上使用
g++ -S -O3 -mavx -masm=intel -mabi=ms foo.cpp
从GCC中反汇编得出:vmovapd ymm0, YMMWORD PTR [rdx]
mov rax, rcx
vaddpd ymm0, ymm0, YMMWORD PTR [r8]
vmovapd YMMWORD PTR [rcx], ymm0
vzeroupper
ret
vaddpd ymm0, ymm0, YMMWORD PTR [rdx]
指令将负载和加法融合到一个融合的微操作中。当我将该函数与您的代码一起使用时,它将得到2,4,6,8。您可以在l1-memory-bandwidth-50-drop-in-efficiency-using-addresses-which-differ-by-4096处找到将两个数组
x
和y
求和的源代码,并将它们写出到z
数组中。这将使用内部函数并将其展开八次。使用gcc -S
或objdump -d
取消该代码。另一个几乎相同的事情,也是用汇编语言编写的,位于obtaining-peak-bandwidth-on-haswell-in-the-l1-cache-only-getting-62。在文件triad_fma_asm.asm
中,将pi: dd 3.14159
行更改为pi: dd 1.0
。这两个示例都使用单个浮点,因此,如果要加倍,则必须进行必要的更改。您其他问题的答案是:
每个内核具有16个通用寄存器(例如rax,rbx,r8,r9等)和几个专用寄存器(例如RFLAGS)。在32位模式下,每个内核都有8个256位寄存器,在64位模式下,则有16个256位寄存器。当AVX-512可用时,将有32个512位寄存器(但在32位模式下只有8个)。
请注意,每个内核都具有far more registers,而不是您可以直接编程的逻辑内核。
微操作融合可以融合例如一个负载和一个附加负载,即所谓的融合微操作,但每个微操作仍需要自己的端口。宏运算融合可以融合例如标量加法和跳入仅需要一个端口的微型运算。宏操作融合本质上是一对二的。
Haswell有八个端口。您可以使用七个端口在一个时钟周期内获得六个微运算。
256-load + 256-FMA //one fused µop using two ports
256-load + 256-FMA //one fused µop using two ports
256-store //one µop using two ports
64-bit add + jump //one µop using one port
因此,实际上,Haswell的每个内核都可以在一个时钟周期内处理16个 double (每个FMA进行4个乘法和4个加法),2个256负载,1个256位存储以及1个64位加法和分支。在这个问题obtaining-peak-bandwidth-on-haswell-in-the-l1-cache-only-getting-62中,我(理论上)使用六个端口在一个时钟周期内获得了五个微运算。但是,实际上在Haswell上很难做到这一点。
对于您的特定操作,它读取两个数组并写入一个数组,因此每个时钟周期受两次读取的约束,因此每个时钟周期只能发出一个FMA。因此,最好的办法是每个时钟周期增加四倍。
但是,让我告诉您一个小 secret ,即英特尔不希望人们谈论太多。 Most operations are memory bandwidth bound,并不能从并行化中受益良多。这包括您问题中的操作。因此,尽管英特尔每隔几年就会不断推出新技术(例如,AVX,FMA,AVX512,将内核数量增加一倍),这每次都会使性能翻倍,从而声称在实践中获得摩尔定律,但平均 yield 是线性的,而不是指数级的现在已经有好几年了。