我已经使用 AVX2 的内在指令对矩阵加法的内部循环进行了向量化,我还有来自 here 的延迟表.我预计加速应该是 5 倍,因为在 1024 次迭代中几乎有 4 次延迟发生在 128 次迭代中有 6 次延迟,但加速是 3 倍。所以问题是这里还有什么我没有看到。我正在使用 gcc,在 c 中编码,内在函数,CPU 是 skylake 6700hq
这是内循环的 c 和汇编输出。
全局数据:
int __attribute__(( aligned(32))) a[MAX1][MAX2] ;
int __attribute__(( aligned(32))) b[MAX2][MAX3] ;
int __attribute__(( aligned(32))) c_result[MAX1][MAX3] ;
顺序:
for( i = 0 ; i < MAX1 ; i++)
for(j = 0 ; j < MAX2 ; j++)
c_result[i][j] = a[i][j] + b[i][j];
.L16:
movl (%r9,%rax), %edx // latency : 2 , throughput : 0.5 number of execution unit : 4 ALU
addl (%r8,%rax), %edx // latency : dont know , throughput : 0.5 number of execution unit : 4 ALU
movl %edx, c_result(%rcx,%rax) // latency : 2 , throughput : 1 number of execution unit : 4 ALU
addq $4, %rax
cmpq $4096, %rax
jne .L16
AVX2:
for( i = 0 ; i < MAX1 ; i++){
for(j = 0 ; j < MAX2 ; j += 8){
a0_i= _mm256_add_epi32( _mm256_load_si256((__m256i *)&a[i][j]) , _mm256_load_si256((__m256i *)&b[i][j]));
_mm256_store_si256((__m256i *)&c_result[i][j], a0_i);
}}
.L22:
vmovdqa (%rcx,%rax), %ymm0 // latency : 3 , throughput : 0.5 number of execution unit : 4 ALU
vpaddd (%r8,%rax), %ymm0, %ymm0 // latency : dont know , throughput : 0.5 number of execution unit : 3 VEC-ALU
vmovdqa %ymm0, c_result(%rdx,%rax) // latency : 3 , throughput : 1 number of execution unit : 4 ALU
addq $32, %rax
cmpq $4096, %rax
jne .L22
最佳答案
除了循环计数器,没有循环携带的依赖链。因此来自不同循环迭代的操作可以同时运行。这意味着延迟不是瓶颈,只是吞吐量(执行单元和前端(每个时钟最多 4 个融合域微指令))。
此外,您的数字非常疯狂。 mov
加载不需要 4 个 ALU 执行单元!并且加载/存储延迟数字是错误的/无意义的(请参阅最后一节)。
# Scalar (serial is the wrong word. Both versions are serial, not parallel)
.L16:
movl (%r9,%rax), %edx // fused-domain uops: 1. Unfused domain: a load port
addl (%r8,%rax), %edx // fused-domain uops: 2 Unfused domain: a load port and any ALU port
movl %edx, c_result(%rcx,%rax) // fused-domain uops: 2 Unfused domain: store-address and store-data ports. port7 can't handle 2-reg addresses
addq $4, %rax // fused-domain uops: 1 unfused: any ALU
cmpq $4096, %rax // fused-domain uops: 0 (fused with jcc)
jne .L16 // fused-domain uops: 1 unfused: port6 (predicted-taken branch)
总计:7 个融合域微指令意味着循环可以每 2c 一次迭代从循环缓冲区发出。 (不是每 1.75c)。由于我们混合使用加载、存储和 ALU 微指令,执行端口不是瓶颈,只是融合域 4 宽问题宽度。每 2c 两次加载和每 2c 一次存储只是加载和存储执行单元吞吐量的一半。
注意 2 寄存器寻址模式 can't micro-fuse on Intel SnB-family .这对于纯负载来说不是问题,因为即使没有微融合,它们也是 1 uop。
vector 循环的分析是相同的。 (vpadd
在 Skylake 和几乎所有其他 CPU 上都有 1c 的延迟。该表没有在带有内存操作数的 padd
的延迟列中列出任何内容,因为延迟加载的延迟与添加的延迟是分开的。它只向涉及寄存器 src/dest 的 dep 链添加一个周期,只要提前足够远地知道加载地址。)
Agner Fog 的存储和加载延迟数字也有点假。他任意地将总的加载-存储往返延迟(带存储转发)分成加载和存储的延迟数。 IDK 为什么他没有列出通过指针跟踪测试测量的加载延迟(例如重复 mov (%rsi), %rsi
)。这表明英特尔 SnB 系列 CPU 具有 4 个周期的加载使用延迟。
我打算给他发一封关于此事的便条,但还没抽出时间。
您应该看到 32/4 的 AVX2 加速,即 8 倍。您的问题大小仅为 4096B,对于三个相同大小的数组来说足够小以适合 L1 缓存。 (编辑:这个问题具有误导性:显示的循环是嵌套循环的内部循环。请参阅评论:显然即使使用 4k 数组(不是 4M),OP 仍然只看到 3 倍的加速(与 4M 阵列的 1.5 倍相比),因此 AVX 版本中存在某种瓶颈。)
所有 3 个数组都是对齐的,所以它不是缓存行交叉
不需要对齐的内存操作数 (%r8
)。
我的其他理论似乎也不太可能,但是您的数组地址彼此偏移正好 4096B 吗?来自 Agner Fog 的微架构 PDF:
It is not possible to read and write simultaneously from addresses that are spaced by a multiple of 4 Kbytes
不过,这个例子显示了一个商店然后加载,所以如果真的能解释的话,那么 IDK。即使内存排序硬件认为加载和存储可能位于同一地址,我也不确定为什么这会阻止代码维持尽可能多的内存操作,或者为什么它会比标量代码更糟糕地影响 AVX2 代码.
尝试将数组彼此偏移额外的 128B 或 256B 或其他值是值得的。
关于c - 为什么使用 AVX2 的加速比预期的要低?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/36247281/