为什么使用 AVX2 的加速比预期的要低?
Why the speedup is lower than expected by using AVX2?
我已经使用 AVX2 的内在指令对矩阵加法的内部循环进行了向量化,我也有 here 的延迟 table。我预计加速应该是 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 , %rax
cmpq 96, %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 , %rax
cmpq 96, %rax
jne .L22
除了循环计数器,没有循环携带的依赖链。因此来自不同循环迭代的操作可以同时运行。这意味着延迟不是瓶颈,只是吞吐量(执行单元和前端(每个时钟最多 4 个融合域 uops))。
另外,你的数字太疯狂了。 mov
加载不占用4个ALU执行单元! load/store 延迟数字是错误的/无意义的(请参阅最后一节)。
# 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 , %rax // fused-domain uops: 1 unfused: any ALU
cmpq 96, %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。
向量循环的分析是相同的。 (vpaddd
在 Skylake 上有 1c 的延迟,几乎所有其他 CPU。table 没有在带有内存操作数的 padd
的延迟列中列出任何内容,因为加载的延迟与添加的延迟是分开的。它只向涉及寄存器 src/dest 的 dep 链添加一个周期,只要提前足够远地知道加载地址。)
Agner Fog 的存储和加载延迟数字也有点假。他任意地将总的加载-存储往返延迟(带存储转发)分成加载和存储的延迟数。 IDK 为什么他没有列出通过指针追踪测试测量的加载延迟(例如重复 mov (%rsi), %rsi
)。这表明英特尔 SnB 系列 CPU 具有 4 个周期的加载使用延迟。
我本来打算给他发一封关于此事的便条,但还没抽出时间。
您应该看到 AVX2 加速为 32/4,即 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 或其他内容。
以下限制限制了两种实现的性能。首先,除了循环计数器之外,没有循环携带的依赖链,因此来自不同循环迭代的操作可以同时执行,这意味着延迟不是主要瓶颈,但延迟是 HPC 中的一个重要因素。由于延迟是相等的,因此执行单元的吞吐量对于两种实现都更有效。 IACA 将标量实现的吞吐量瓶颈演示为“迭代间”,这意味着循环的连续迭代之间存在依赖关系,矢量化有助于使代码 运行 faster.furthermore,可以发出矢量化模式下的 vpaddd在端口 5,1 上,但当第一个周期中端口 0 忙时,add 使用执行端口 1、5、6。其次,融合域前端的吞吐量可能会影响性能,但根据 IACA 结果,在该算法中,每次迭代需要 7 微指令,HSW/SKL 微架构最多可以发出 4 微指令。每个时钟的融合域 uops,因此每次内循环迭代需要 2 个周期,并且此限制比标量实现更违反 AVX2 实现。第三,算法的数据依赖性导致许多缓存未命中。通过减小适合 L1D(一级数据缓存)的矩阵的大小,变成了 5 倍(我怎么测试了很多次得到 5 但 IDK 再次测试加速是 7.3).
我已经使用 AVX2 的内在指令对矩阵加法的内部循环进行了向量化,我也有 here 的延迟 table。我预计加速应该是 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 , %rax
cmpq 96, %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 , %rax
cmpq 96, %rax
jne .L22
除了循环计数器,没有循环携带的依赖链。因此来自不同循环迭代的操作可以同时运行。这意味着延迟不是瓶颈,只是吞吐量(执行单元和前端(每个时钟最多 4 个融合域 uops))。
另外,你的数字太疯狂了。 mov
加载不占用4个ALU执行单元! load/store 延迟数字是错误的/无意义的(请参阅最后一节)。
# 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 , %rax // fused-domain uops: 1 unfused: any ALU
cmpq 96, %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。
向量循环的分析是相同的。 (vpaddd
在 Skylake 上有 1c 的延迟,几乎所有其他 CPU。table 没有在带有内存操作数的 padd
的延迟列中列出任何内容,因为加载的延迟与添加的延迟是分开的。它只向涉及寄存器 src/dest 的 dep 链添加一个周期,只要提前足够远地知道加载地址。)
Agner Fog 的存储和加载延迟数字也有点假。他任意地将总的加载-存储往返延迟(带存储转发)分成加载和存储的延迟数。 IDK 为什么他没有列出通过指针追踪测试测量的加载延迟(例如重复 mov (%rsi), %rsi
)。这表明英特尔 SnB 系列 CPU 具有 4 个周期的加载使用延迟。
我本来打算给他发一封关于此事的便条,但还没抽出时间。
您应该看到 AVX2 加速为 32/4,即 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 或其他内容。
以下限制限制了两种实现的性能。首先,除了循环计数器之外,没有循环携带的依赖链,因此来自不同循环迭代的操作可以同时执行,这意味着延迟不是主要瓶颈,但延迟是 HPC 中的一个重要因素。由于延迟是相等的,因此执行单元的吞吐量对于两种实现都更有效。 IACA 将标量实现的吞吐量瓶颈演示为“迭代间”,这意味着循环的连续迭代之间存在依赖关系,矢量化有助于使代码 运行 faster.furthermore,可以发出矢量化模式下的 vpaddd在端口 5,1 上,但当第一个周期中端口 0 忙时,add 使用执行端口 1、5、6。其次,融合域前端的吞吐量可能会影响性能,但根据 IACA 结果,在该算法中,每次迭代需要 7 微指令,HSW/SKL 微架构最多可以发出 4 微指令。每个时钟的融合域 uops,因此每次内循环迭代需要 2 个周期,并且此限制比标量实现更违反 AVX2 实现。第三,算法的数据依赖性导致许多缓存未命中。通过减小适合 L1D(一级数据缓存)的矩阵的大小,变成了 5 倍(我怎么测试了很多次得到 5 但 IDK 再次测试加速是 7.3).