为什么使用AVX ymm(m256)指令比xmm(m128)慢大约4倍?

5

我编写了一个程序,它可以将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

1
如果您正在使用C语言,可以删除masm和performance标签,然后添加x86、c和汇编。 - S.S. Anne
1
有多慢?你的问题似乎没有任何数字。 - Peter Cordes
~慢了4倍 - Aleksander Schultz
如果您的数组大小不是32的倍数,则在YMM版本中,“ResidualsLoopHead”部分将执行更多次。如果您的数组平均大小较小,这将变得非常重要。 - W. Chang
1个回答

7
你旧的FX-6300处理器使用的是AMD Piledriver微架构
它将256位指令解码为两个128位的微操作码(与Zen 2之前的所有AMD处理器一样)。因此,你通常不会在该处理器上从AVX中获得加速,并且2个微操作码有时可能会成为前端瓶颈。尽管与Bulldozer不同,它可以在一个周期内解码2个微操作码,因此2个微操作码指令序列的解码速度可以达到每个时钟周期4个微操作码,与单个微操作码指令序列相同。
能够运行AVX指令对于避免movaps寄存器复制指令非常有用,同时也能够运行与英特尔处理器相同的代码(因为英特尔处理器具有256位宽的执行单元)。 你的问题可能是Piledriver在256位存储上存在一个严重性能缺陷。(Bulldozer中不存在,在Steamroller / Excavator中已修复。)根据Agner Fog's微架构PDF,在Bulldozer系列的部分:该微架构的AVX缺点如下:

与Bulldozer和Piledriver上128位存储指令的吞吐量相比,256位存储指令的吞吐量不到一半。Piledriver特别糟糕,每17到20个时钟周期只能执行一次256位存储操作

(相对于每个时钟周期执行一次128位存储操作)。我认为即使是在L1d缓存中也会有这种情况。(或者在写合并缓冲区中;Bulldozer系列使用的是写透方式的L1d缓存,是普遍被认为是设计错误的。)

如果这是个问题,使用vmovups [mem], xmmvextractf128 [mem], ymm, 1应该会有很大帮助。您可以尝试保持循环的其余部分为256位。 (然后它应该和128位循环性能相当。您可以减少展开以使两个循环中的工作量相同,并且仍然有效地有4个依赖链,但代码大小更小。或者保持在4个寄存器上,以获得8个128位FP乘法依赖链,每个256位寄存器具有两个半部分。)
请注意,如果您可以选择对齐加载或对齐存储,请选择对齐存储。根据Agner's指令表,vmovapd [mem], ymm(17个周期吞吐量,4个uop)不像vmovupd [mem], ymm(20个周期吞吐量,8个uop)那样糟糕。但与Piledriver上的2-uop 1周期vextractf128 + 1-uop vmovupd xmm相比,两者都很差。
另一个缺点(这不适用于您的代码,因为它没有reg-reg vmovaps指令):
128位寄存器到寄存器的移动具有零延迟,而256位寄存器到寄存器的移动在Bulldozer和Piledriver上的延迟为2个时钟周期,加上使用不同域的惩罚为2-3个时钟周期。通过非破坏性的三操作数指令,大多数情况下可以避免使用寄存器到寄存器的移动。
(低128位受益于mov消除;高128位则通过后端uop单独移动。)

3
有关Piledriver中的性能错误是否已经确定了具体原因? - harold
1
@哈罗德:我还没有看过相关的内容。好问题,现在我也很好奇!(但是我现在不太好奇去谷歌搜索它。也许稍后再说吧。) - Peter Cordes
3
Agner通常不记录跨页情况,但他指出,在Bulldozer/Piledriver/Streamroller架构中,跨越(4KiB)分页边界的非对齐加载操作的吞吐量为每21个时钟周期执行1次。如果我没记错的话,跨页边界的非对齐存储要更差,严重降低吞吐量(即使跨其他缓存行边界的存储操作的惩罚也不大)。 - John D McCalpin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接