为什么使用 AVX ymm(m256) 指令比 xmm(m128) 慢 ~4 倍
Why using AVX ymm(m256) instructions is ~4 times slower than xmm(m128)
我编写了程序,将 arr1*arr2 相乘并将结果保存到 arr3。
Pseudocode:
arr3[i]=arr1[i]*arr2[i]
而且我想使用AVX指令。我有 m128 和 m256 指令的汇编代码(展开)。
结果表明,使用 ymm 比 xmm 慢 4 倍。但为什么?如果延迟相同..
Mul_ASM_AVX proc ; (float* RCX=arr1, float* RDX=arr2, float* R8=arr3, int R9 = arraySize)
push rbx
vpxor xmm0, xmm0, xmm0 ; Zero the counters
vpxor xmm1, xmm1, xmm1
vpxor xmm2, xmm2, xmm2
vpxor xmm3, xmm3, xmm3
mov rbx, r9
sar r9, 4 ; Divide the count by 16 for AVX
jz MulResiduals ; If that's 0, then we have only scalar mul to perfomance
LoopHead:
;add 16 floats
vmovaps xmm0 , xmmword ptr[rcx]
vmovaps xmm1 , xmmword ptr[rcx+16]
vmovaps xmm2 , xmmword ptr[rcx+32]
vmovaps xmm3 , xmmword ptr[rcx+48]
vmulps xmm0, xmm0, xmmword ptr[rdx]
vmulps xmm1, xmm1, xmmword ptr[rdx+16]
vmulps xmm2, xmm2, xmmword ptr[rdx+32]
vmulps xmm3, xmm3, xmmword ptr[rdx+48]
vmovaps xmmword ptr[R8], xmm0
vmovaps xmmword ptr[R8+16], xmm1
vmovaps xmmword ptr[R8+32], xmm2
vmovaps xmmword ptr[R8+48], xmm3
add rcx, 64 ; move on to the next 16 floats (4*16=64)
add rdx, 64
add r8, 64
dec r9
jnz LoopHead
MulResiduals:
and ebx, 15 ; do we have residuals?
jz Finished ; If not, we're done
ResidualsLoopHead:
vmovss xmm0, real4 ptr[rcx]
vmulss xmm0, xmm0, real4 ptr[rdx]
vmovss real4 ptr[r8], xmm0
add rcx, 4
add rdx, 4
dec rbx
jnz ResidualsLoopHead
Finished:
pop rbx ; restore caller's rbx
ret
Mul_ASM_AVX endp
而对于m256,ymm说明:
Mul_ASM_AVX_YMM proc ; UNROLLED AVX
push rbx
vzeroupper
mov rbx, r9
sar r9, 5 ; Divide the count by 32 for AVX (8 floats * 4 registers = 32 floats)
jz MulResiduals ; If that's 0, then we have only scalar mul to perfomance
LoopHead:
;add 32 floats
vmovaps ymm0, ymmword ptr[rcx] ; 8 float each, 8*4 = 32
vmovaps ymm1, ymmword ptr[rcx+32]
vmovaps ymm2, ymmword ptr[rcx+64]
vmovaps ymm3, ymmword ptr[rcx+96]
vmulps ymm0, ymm0, ymmword ptr[rdx]
vmulps ymm1, ymm1, ymmword ptr[rdx+32]
vmulps ymm2, ymm2, ymmword ptr[rdx+64]
vmulps ymm3, ymm3, ymmword ptr[rdx+96]
vmovupd ymmword ptr[r8], ymm0
vmovupd ymmword ptr[r8+32], ymm1
vmovupd ymmword ptr[r8+64], ymm2
vmovupd ymmword ptr[r8+96], ymm3
add rcx, 128 ; move on to the next 32 floats (4*32=128)
add rdx, 128
add r8, 128
dec r9
jnz LoopHead
MulResiduals:
and ebx, 31 ; do we have residuals?
jz Finished ; If not, we're done
ResidualsLoopHead:
vmovss xmm0, real4 ptr[rcx]
vmulss xmm0, xmm0, real4 ptr[rdx]
vmovss real4 ptr[r8], xmm0
add rcx, 4
add rdx, 4
dec rbx
jnz ResidualsLoopHead
Finished:
pop rbx ; restore caller's rbx
ret
Mul_ASM_AVX_YMM endp
CPU-Z报告:
- 制造商:AuthenticAMD
- 名称:AMD FX-6300 代号:Vishera
- 规格:AMD FX(tm)-6300 六核处理器
- CPUID: F.2.0
- 扩展CPUID: 15.2
- 技术:32纳米
- 指令集MMX(+)、SSE、SSE2、SSE3、SSSE3、SSE4.1、SSE4.2,
SSE4A、x86-64、AMD-V、AES、AVX、XOP、FMA3、FMA4
您的旧 FX-6300 中的核心是 AMD Piledriver microarchitecture。
它将 256 位指令解码为两个 128 位微指令。 (就像 Zen 2 之前的所有 AMD 一样)。因此,您通常不会期望 AVX 在 CPU 上获得加速,并且 2-uop 指令有时会成为前端的瓶颈。尽管与 Bulldozer 不同,它可以在 1 个周期内解码 2-2 微指令模式,因此 2 微指令序列可以以每个时钟 4 微指令的速率解码,与单微指令序列相同。
能够 运行 AVX 指令对于避免 movaps 寄存器复制指令很有用,并且能够 运行 与英特尔 CPUs 相同的代码(确实有 256位宽的执行单元)。
您的问题可能是 Piledriver 在 256 位存储中存在性能问题。 (在 Bulldozer 中不存在,在 Steamroller / Excavator 中修复。)来自 Agner Fog's microarch PDF,在 Bulldozer 系列部分:AVX 在该微体系结构上的缺点:
The throughput of 256-bit store instructions is less than half the throughput of 128-bit
store instructions on Bulldozer and Piledriver. It is particularly bad on the Piledriver,
which has a throughput of one 256-bit store per 17 - 20 clock cycles
(相对于每个时钟一个 128 位存储)。我认为这甚至适用于命中 L1d 缓存的商店。 (或者在写入组合缓冲区中;Bulldozer 系列使用直写式 L1d 缓存,是的,这通常被认为是设计错误。)
如果这是问题所在,使用 vmovups [mem], xmm
和 vextractf128 [mem], ymm, 1
应该可以帮助 很多。您可以尝试将循环的其余部分保持为 128 位。(然后它应该执行大约等于 128 位循环。您可以减少展开以在两个循环中获得相同数量的工作并且仍然实际上是 4 个 dep 链,但代码量更小。或者将其保持在 4 个寄存器以获得 8x 128 位 FP 乘法 dep 链,每个 256 位寄存器有两半。)
请注意,如果您可以在对齐加载或对齐商店之间进行选择,请选择对齐商店。根据 Agner 的说明 table,vmovapd [mem], ymm
(17 周期吞吐量,4 微指令)不如 vmovupd [mem], ymm
(20 周期吞吐量,8 微指令)那么糟糕。但与打桩机上的 2-uop 1 周期 vextractf128
+ 1-uop vmovupd xmm
相比,两者都很糟糕。
另一个缺点(不适用于您的代码,因为它没有 reg-reg vmovaps 指令):
128-bit register-to-register moves have zero latency, while 256-bit register-to-register
moves have a latency of 2 clocks plus a penalty of 2-3 clocks for using a different
domain (see below) on Bulldozer and Piledriver. Register-to-register moves can be
avoided in most cases thanks to the non-destructive 3-operand instructions.
(低 128 位受益于移动消除;高 128 位通过后端 uop 单独移动。)
我编写了程序,将 arr1*arr2 相乘并将结果保存到 arr3。
Pseudocode:
arr3[i]=arr1[i]*arr2[i]
而且我想使用AVX指令。我有 m128 和 m256 指令的汇编代码(展开)。 结果表明,使用 ymm 比 xmm 慢 4 倍。但为什么?如果延迟相同..
Mul_ASM_AVX proc ; (float* RCX=arr1, float* RDX=arr2, float* R8=arr3, int R9 = arraySize)
push rbx
vpxor xmm0, xmm0, xmm0 ; Zero the counters
vpxor xmm1, xmm1, xmm1
vpxor xmm2, xmm2, xmm2
vpxor xmm3, xmm3, xmm3
mov rbx, r9
sar r9, 4 ; Divide the count by 16 for AVX
jz MulResiduals ; If that's 0, then we have only scalar mul to perfomance
LoopHead:
;add 16 floats
vmovaps xmm0 , xmmword ptr[rcx]
vmovaps xmm1 , xmmword ptr[rcx+16]
vmovaps xmm2 , xmmword ptr[rcx+32]
vmovaps xmm3 , xmmword ptr[rcx+48]
vmulps xmm0, xmm0, xmmword ptr[rdx]
vmulps xmm1, xmm1, xmmword ptr[rdx+16]
vmulps xmm2, xmm2, xmmword ptr[rdx+32]
vmulps xmm3, xmm3, xmmword ptr[rdx+48]
vmovaps xmmword ptr[R8], xmm0
vmovaps xmmword ptr[R8+16], xmm1
vmovaps xmmword ptr[R8+32], xmm2
vmovaps xmmword ptr[R8+48], xmm3
add rcx, 64 ; move on to the next 16 floats (4*16=64)
add rdx, 64
add r8, 64
dec r9
jnz LoopHead
MulResiduals:
and ebx, 15 ; do we have residuals?
jz Finished ; If not, we're done
ResidualsLoopHead:
vmovss xmm0, real4 ptr[rcx]
vmulss xmm0, xmm0, real4 ptr[rdx]
vmovss real4 ptr[r8], xmm0
add rcx, 4
add rdx, 4
dec rbx
jnz ResidualsLoopHead
Finished:
pop rbx ; restore caller's rbx
ret
Mul_ASM_AVX endp
而对于m256,ymm说明:
Mul_ASM_AVX_YMM proc ; UNROLLED AVX
push rbx
vzeroupper
mov rbx, r9
sar r9, 5 ; Divide the count by 32 for AVX (8 floats * 4 registers = 32 floats)
jz MulResiduals ; If that's 0, then we have only scalar mul to perfomance
LoopHead:
;add 32 floats
vmovaps ymm0, ymmword ptr[rcx] ; 8 float each, 8*4 = 32
vmovaps ymm1, ymmword ptr[rcx+32]
vmovaps ymm2, ymmword ptr[rcx+64]
vmovaps ymm3, ymmword ptr[rcx+96]
vmulps ymm0, ymm0, ymmword ptr[rdx]
vmulps ymm1, ymm1, ymmword ptr[rdx+32]
vmulps ymm2, ymm2, ymmword ptr[rdx+64]
vmulps ymm3, ymm3, ymmword ptr[rdx+96]
vmovupd ymmword ptr[r8], ymm0
vmovupd ymmword ptr[r8+32], ymm1
vmovupd ymmword ptr[r8+64], ymm2
vmovupd ymmword ptr[r8+96], ymm3
add rcx, 128 ; move on to the next 32 floats (4*32=128)
add rdx, 128
add r8, 128
dec r9
jnz LoopHead
MulResiduals:
and ebx, 31 ; do we have residuals?
jz Finished ; If not, we're done
ResidualsLoopHead:
vmovss xmm0, real4 ptr[rcx]
vmulss xmm0, xmm0, real4 ptr[rdx]
vmovss real4 ptr[r8], xmm0
add rcx, 4
add rdx, 4
dec rbx
jnz ResidualsLoopHead
Finished:
pop rbx ; restore caller's rbx
ret
Mul_ASM_AVX_YMM endp
CPU-Z报告:
- 制造商:AuthenticAMD
- 名称:AMD FX-6300 代号:Vishera
- 规格:AMD FX(tm)-6300 六核处理器
- CPUID: F.2.0
- 扩展CPUID: 15.2
- 技术:32纳米
- 指令集MMX(+)、SSE、SSE2、SSE3、SSSE3、SSE4.1、SSE4.2,
SSE4A、x86-64、AMD-V、AES、AVX、XOP、FMA3、FMA4
您的旧 FX-6300 中的核心是 AMD Piledriver microarchitecture。
它将 256 位指令解码为两个 128 位微指令。 (就像 Zen 2 之前的所有 AMD 一样)。因此,您通常不会期望 AVX 在 CPU 上获得加速,并且 2-uop 指令有时会成为前端的瓶颈。尽管与 Bulldozer 不同,它可以在 1 个周期内解码 2-2 微指令模式,因此 2 微指令序列可以以每个时钟 4 微指令的速率解码,与单微指令序列相同。
能够 运行 AVX 指令对于避免 movaps 寄存器复制指令很有用,并且能够 运行 与英特尔 CPUs 相同的代码(确实有 256位宽的执行单元)。
您的问题可能是 Piledriver 在 256 位存储中存在性能问题。 (在 Bulldozer 中不存在,在 Steamroller / Excavator 中修复。)来自 Agner Fog's microarch PDF,在 Bulldozer 系列部分:AVX 在该微体系结构上的缺点:
The throughput of 256-bit store instructions is less than half the throughput of 128-bit store instructions on Bulldozer and Piledriver. It is particularly bad on the Piledriver, which has a throughput of one 256-bit store per 17 - 20 clock cycles
(相对于每个时钟一个 128 位存储)。我认为这甚至适用于命中 L1d 缓存的商店。 (或者在写入组合缓冲区中;Bulldozer 系列使用直写式 L1d 缓存,是的,这通常被认为是设计错误。)
如果这是问题所在,使用 vmovups [mem], xmm
和 vextractf128 [mem], ymm, 1
应该可以帮助 很多。您可以尝试将循环的其余部分保持为 128 位。(然后它应该执行大约等于 128 位循环。您可以减少展开以在两个循环中获得相同数量的工作并且仍然实际上是 4 个 dep 链,但代码量更小。或者将其保持在 4 个寄存器以获得 8x 128 位 FP 乘法 dep 链,每个 256 位寄存器有两半。)
请注意,如果您可以在对齐加载或对齐商店之间进行选择,请选择对齐商店。根据 Agner 的说明 table,vmovapd [mem], ymm
(17 周期吞吐量,4 微指令)不如 vmovupd [mem], ymm
(20 周期吞吐量,8 微指令)那么糟糕。但与打桩机上的 2-uop 1 周期 vextractf128
+ 1-uop vmovupd xmm
相比,两者都很糟糕。
另一个缺点(不适用于您的代码,因为它没有 reg-reg vmovaps 指令):
128-bit register-to-register moves have zero latency, while 256-bit register-to-register moves have a latency of 2 clocks plus a penalty of 2-3 clocks for using a different domain (see below) on Bulldozer and Piledriver. Register-to-register moves can be avoided in most cases thanks to the non-destructive 3-operand instructions.
(低 128 位受益于移动消除;高 128 位通过后端 uop 单独移动。)