为啥使用 AVX2 的加速比低于预期?
Posted
技术标签:
【中文标题】为啥使用 AVX2 的加速比低于预期?【英文标题】:Why the speedup is lower than expected by using AVX2?为什么使用 AVX2 的加速比低于预期? 【发布时间】:2016-03-27 12:24:15 【问题描述】:我已经使用 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
【问题讨论】:
内存对齐是 32 字节,L1D 缓存线大小是 64 字节和 8 路,我还在研究。但我需要一个专业的领导,是的,我知道今天是星期天。 您尝试过 IACA 吗?它没有做 Skylake,我上次看,但它在 Haswell 上的结果可能会有所帮助。另外,请查看Agner Fog's instruction tables. “IACA”代表“英特尔架构代码分析器”。 @Amir:当然,它与 IACA 捆绑在一起。 IACA 是封闭源代码,IDK 为什么您希望专门在 github 上而不是 google 上找到它。在 asm 中,使用mov $111, %ebx
/ .byte 0x64, 0x67, 0x90
用于 IACA 开始,与 $222
相同用于 IACA 结束。在 32 位模式下,这是一条非法指令(故意:破坏 ebx 会破坏您的代码)。在 64 位模式下,它不是。 (对于 64 位,宏扩展为其他内容,但 iaca
仍可识别 64 位代码中的这些标记。因此,在手写 ASM 中,您通常可以安排内容,以便在测试时留下标记)。
@Amir:不,自从 Haswell 之后它就被放弃了只有 4c,并在 FMA 单元中添加发生,以及其他一些延迟/端口更改可能对某些代码很重要,但分析关键路径以帮助您了解正在发生的事情的总体情况并没有改变。
【参考方案1】:
除了循环计数器,没有循环携带的依赖链。因此,来自不同循环迭代的操作可以同时进行。这意味着延迟不是瓶颈,只是吞吐量(执行单元和前端(每个时钟最多 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 个融合域 uops 意味着循环可以每 2c 一次迭代从循环缓冲区发出。 (不是每 1.75c)。由于我们混合使用了加载、存储和 ALU 微指令,因此执行端口不是瓶颈,只是融合域 4 宽的问题宽度。每 2c 两次加载和每 2c 一次存储仅是加载和存储执行单元吞吐量的一半。
注意 2 寄存器寻址模式 can't micro-fuse on Intel SnB-family。这对于纯负载来说不是问题,因为即使没有微融合,它们也是 1 uop。
向量循环的分析是相同的。 (vpaddd
在 Skylake 和几乎所有其他 CPU 上的延迟为 1c。该表没有在 padd
的延迟列中列出任何带有内存操作数的内容,因为加载的延迟与add。它只在涉及寄存器 src/dest 的 dep 链中增加一个周期,只要提前足够知道加载地址。)
Agner Fog 的存储和加载延迟数字也有点假。他任意将总的加载-存储往返延迟(使用存储转发)划分为加载和存储的延迟数。 IDK 为什么他没有列出通过指针追踪测试测量的加载延迟(例如重复mov (%rsi), %rsi
)。这表明英特尔 SnB 系列 CPU 具有 4 个周期的负载使用延迟。
我本来打算给他发个便条,但还没来得及。
您应该看到 32/4 的 AVX2 加速,即 8 倍。您的问题大小仅为 4096B,对于该大小的三个数组来说足够小以适合 L1 缓存。 (编辑:这个问题具有误导性:显示的循环是嵌套循环的内循环。 查看 cmets:显然即使使用 4k 数组(不是 4M),OP 仍然只能看到 3 倍的加速(与 4M 阵列的 1.5 倍相比),因此 AVX 版本存在某种瓶颈。)
所有 3 个数组都是对齐的,因此在
不需要对齐的内存操作数 (%r8
)。
我对此的其他理论似乎也不太可能,但是您的数组地址是否彼此偏移正好 4096B?来自 Agner Fog 的微架构 PDF:
不能同时从地址读取和写入 间隔为 4 KB 的倍数
该示例显示了一个商店然后加载,所以 IDK 如果这真的解释了它。即使内存排序硬件认为加载和存储可能在同一个地址,我也不知道为什么这会阻止代码维持尽可能多的内存操作,或者为什么它会比标量代码更糟糕地影响 AVX2 代码.
值得尝试通过额外的 128B 或 256B 或其他方式将您的阵列相互抵消。
【讨论】:
@Amir:ALU = 算术和逻辑单元。 Skylake 在端口 0、1、5 和 6 上有 ALU。movl (%r9,%rax), %edx
是纯负载,不需要 ALU。它只需要一个加载端口,SnB 系列 CPU 有两个。这就是为什么它的吞吐量是每 0.5c 一个。
@Amir:这意味着这些指令的reg-reg形式可以在所有4个ALU端口上运行。
@Amir:正确。这就是为什么 Agner Fog 的表格没有列出任何 ALU 端口,只列出了 mov r,m
表单的加载端口。
既然你有一个skylake,你可以编写一个循环来限制指针追逐的延迟,通过vmovdqa (%rax), %ymm0
,vmovq %ymm0, %rax
。 (并减去 vmovq
的 2c 延迟,从 Broadwell 中的 1c 增加:/)
所以你展示的 asm 只是一对嵌套循环的内循环? 每个数组实际上是4MiB = 4B*1024*1024
,而不是4kiB = 4B*1024
。这对于 L3 缓存来说太大了,所以你在主内存上遇到了瓶颈,呵呵。即使MAX=256
是每个阵列的 256kiB,所以它们仍然不适合 L2。 L3 缓存比 DRAM 快,但在任何地方接近都没有 L1 快。当然,您不会看到像巨型阵列的 8 倍加速和如此低的计算与数据传输比率。 uop 计数,当您可能每时钟获得约 1 uop 时,所有这些都无关紧要。使用perf
。【参考方案2】:
以下限制限制了两种实现的性能。首先,除了循环计数器之外,没有循环携带的依赖链,因此可以同时执行来自不同循环迭代的操作,这意味着延迟不是主要瓶颈,但延迟是 HPC 中的一个重要因素。由于延迟是相同的,因此执行单元的吞吐量对于两种实现都更有效。 IACA 将标量实现的吞吐量瓶颈演示为“Inter-Iteration”,这意味着循环的连续迭代之间存在依赖关系,矢量化有助于使代码运行得更快。此外,矢量化模式下的 vpaddd 可以在端口 5,1 上发出,但当端口 0 在第一个周期忙时,add 使用执行端口 1、5、6。其次,融合域前端的吞吐量可能会影响性能,但在此算法中,根据两种实现的 IACA 结果,每次迭代需要 7 个微指令,HSW/SKL 微架构最多可以发出 4 个融合每个时钟域 uops,因此内部循环的每次迭代需要 2 个周期,并且这种限制比标量实现更违反 AVX2 实现。第三,算法的数据依赖性导致很多缓存未命中。通过减小矩阵的大小以适合 L1D(一级数据缓存)成为 5 倍(我测试了很多次才得到 5,但 IDK 再次测试加速是 7.3)。
【讨论】:
有趣的是,您获得了 5 倍的加速,而不是 8 倍,因为标量与 AVX2 的延迟和微指令是相同的。另请注意,IACA 的total
是未融合域的微指令,这不是一个有用的东西。 (例如,异或归零和消除的移动被视为零)。在您的情况下,答案是相同的,因为您的 uops 都不能进行微熔断,只能进行宏熔断。
无论如何,英特尔的优化手册在第 2.1.3 节中给出了 Skylake 上 L1、L2 等的峰值与持续吞吐量的表格。 Skylake 只能维持约 81B/周期的总进出 L1D 缓存。 (Haswell 表没有该列。IDK 是否意味着持续=峰值)。但是,我刚刚意识到这并不能解释有关循环的标量与矢量的任何内容,因为前端将您的代码限制为每 2 个循环 96B。我想了一分钟我找到了解释,但我想没有。
矢量化模式下的 vpaddd 可以在端口 5,1 上发出,但是当端口 0 在第一个周期中繁忙时,add 使用执行端口 1,5,6。刚刚添加到答案中
我也不知道是什么将持续吞吐量限制在 81B。估计是实验测量。峰值吞吐量仍列为预期的每个周期 96B(2x32B 负载,32B 存储)。自 Haswell 以来的 Intel CPU 在缓存子系统中有 256b 数据路径。
执行单元是完全流水线的,所以它们不会被“占用”,直到一个 insn 退休或类似的事情。您的两个循环都不会对任何 ALU 执行端口施加任何重大压力。在具有更高计算与内存访问比率的密集代码中,是的,add
的吞吐量为每 0.25c 一个,而vpaddd
只能执行到端口 0/1/5。另外:re:81B/clock,好点:存储地址 uops 窃取端口 2,3 个周期确实发生了,这可能是限制持续吞吐量的原因。如果这就是你想说的话,IDK。以上是关于为啥使用 AVX2 的加速比低于预期?的主要内容,如果未能解决你的问题,请参考以下文章