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

Ale*_*ltz 4 x86 assembly sse avx amd-processor

我编写了乘以 arr1*arr2 并将结果保存到 arr3 的程序。

Pseudocode:
arr3[i]=arr1[i]*arr2[i]
Run Code Online (Sandbox Code Playgroud)

我想使用 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
Run Code Online (Sandbox Code Playgroud)

对于 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
Run Code Online (Sandbox Code Playgroud)

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

Pet*_*des 6

旧 FX-6300 中的内核是AMD Piledriver 微架构

它将 256 位指令解码为两个 128 位微指令。(就像 Zen 2 之前的所有 AMD)。因此,您通常不会期望 AVX在该 CPU 上实现加速,并且 2-uop 指令有时会成为前端的瓶颈。尽管与推土机不同,它可以在 1 个周期内解码 2-2 个 uop 模式,因此 2 个 uop 指令序列可以以每个时钟 4 个 uop 的速率解码,与单 uop 指令序列相同。

能够运行 AVX 指令对于避免 movaps 寄存器复制指令很有用,并且还能够运行与 Intel CPU(具有 256 位宽的执行单元)相同的代码。

您的问题可能是 Piledriver 在 256-bit stores 上有一个 showstopper 性能错误。(在推土机中不存在,在蒸汽压路机/挖掘机中修复。)来自Agner Fog 的微架构 PDF,在推土机系列部分: AVX 在该微架构上的缺点:

在推土机和打桩机上,256 位存储指令的吞吐量不到 128 位存储指令吞吐量的一半。在 Piledriver 上尤其糟糕,它的吞吐量为每 17 - 20 个时钟周期一个 256 位存储

(相对于每个时钟一个 128 位存储)。我认为这甚至适用于在 L1d 缓存中命中的存储。(或者在写组合缓冲区中;Bulldozer 系列使用直写 L1d 缓存,是的,这通常被认为是设计错误。)

如果这是问题所在,那么使用vmovups [mem], xmmandvextractf128 [mem], ymm, 1应该会有很大帮助。您可以尝试将循环的其余部分保持为 128 位。 (然后它应该执行大约等于 128 位循环。您可以减少展开以在两个循环中获得相同的工作量,并且仍然有效地使用 4 个 dep 链,但代码大小更小。或者将其保持在 4 个寄存器以获得 8 个 128 位 FP 乘法深度链,每个 256 位寄存器有两半。)

请注意,如果您可以在对齐的负载或对齐的商店之间进行选择,请选择对齐的商店。根据 Agner 的指令表,vmovapd [mem], ymm (17 周期吞吐量,4 uop)并不像vmovupd [mem], ymm(20 周期吞吐量,8 uop)那么糟糕。但与Piledriver 上的2-uop 1 cycle vextractf128+ 1-uop相比,两者都太糟糕了vmovupd xmm


另一个缺点(不适用于您的代码,因为它没有 reg-reg vmovaps 指令):

128 位寄存器到寄存器移动具有零延迟,而 256 位寄存器到寄存器移动具有 2 个时钟的延迟加上在推土机和打桩机上使用不同域(见下文)的 2-3 个时钟的惩罚. 由于非破坏性的 3 操作数指令,在大多数情况下可以避免寄存器到寄存器的移动。

(低 128 位受益于移动消除;高 128 位通过后端 uop 单独移动。)

  • Agner 通常不会记录页面交叉情况,但他指出,在 Bulldozer/Piledriver/Streamroller 上,跨 (4KiB) 页面边界的未对齐加载以每 21 个周期 1 次的吞吐量执行。如果我没记错的话,跨页边界的未对齐存储要糟糕得多,并且会显着降低吞吐量(即使跨其他缓存行边界的存储的惩罚并不大)。 (3认同)
  • 是否有任何关于导致打桩机性能错误的具体原因的信息? (2认同)