首页 > 解决方案 > 为什么对齐限制会在矢量化时改变 clang 的行为?

问题描述

谁能解释一下clang的行为?

Array4Complex64 f1(Array4Complex64 a, Array4Complex64 b){
    return a * b;
}

此函数计算 a 和 b 中的每个元素的复数乘积。我编译了两次,一次对 Array4Complex64 类型有对齐限制,一次没有。结果如下:

对齐:

f1(Array4Complex64, Array4Complex64):               # @f1(Array4Complex64, Array4Complex64)
    push    rbp
    mov     rbp, rsp
    and     rsp, -32
    sub     rsp, 32
    mov     rax, rdi
    vmovapd ymm0, ymmword ptr [rbp + 80]
    vmovapd ymm1, ymmword ptr [rbp + 112]
    vmovapd ymm2, ymmword ptr [rbp + 16]
    vmovapd ymm3, ymmword ptr [rbp + 48]
    vmulpd  ymm4, ymm1, ymm3
    vfmsub231pd     ymm4, ymm0, ymm2        # ymm4 = (ymm0 * ymm2) - ymm4
    vmovapd ymmword ptr [rdi], ymm4
    vmulpd  ymm0, ymm3, ymm0
    vfmadd231pd     ymm0, ymm1, ymm2        # ymm0 = (ymm1 * ymm2) + ymm0
    vmovapd ymmword ptr [rdi + 32], ymm0
    mov     rsp, rbp
    pop     rbp
    vzeroupper
    ret

没有:

f1(Array4Complex64, Array4Complex64):               # @f1(Array4Complex64, Array4Complex64)
    mov     rax, rdi
    vmovupd ymm0, ymmword ptr [rsp + 72]
    vmovupd ymm1, ymmword ptr [rsp + 104]
    vmovupd ymm2, ymmword ptr [rsp + 8]
    vmovupd ymm3, ymmword ptr [rsp + 40]
    vmulpd  ymm4, ymm1, ymm3
    vfmsub231pd     ymm4, ymm0, ymm2        # ymm4 = (ymm0 * ymm2) - ymm4
    vmovupd ymmword ptr [rdi], ymm4
    vmulpd  ymm0, ymm3, ymm0
    vfmadd231pd     ymm0, ymm1, ymm2        # ymm0 = (ymm1 * ymm2) + ymm0
    vmovupd ymmword ptr [rdi + 32], ymm0
    vzeroupper
    ret

结果是一样的,但是地址的计算方式不同:一次是相对于 rbp,一次是相对于 rsp。它不限于乘法,适用于任何计算。一个版本比另一个更好吗?

标签: c++clangx86-64memory-alignmentavx

解决方案


第一种方式是无用的对齐RSP(并在过程中将RBP设置为帧指针,因此自然而然地使用它)。这显然是一个错过的优化,因为它实际上并没有将这些函数参数中的任何一个溢出到堆栈中。(因为这不是调试版本)。您可以将此报告给http://bugs.llvm.org/。包含一个 MCVE 的源代码,以及这个 asm 输出。

不幸的是,这两种方式都是在堆栈内存中传递值,而不是 YMM 寄存器。:( x86-64 System V 可以在 YMM 寄存器中传递一个结构,如果它都是 FP 并且大小为 32 字节。(您可能必须手动将 64 字节参数分解为两个单独的 32 字节参数才能有效传递,如果你不能让它内联,你不能使用 AVX-512 让它在单个 ZMM 寄存器中传递。)

调用者负责在栈上按值传递对齐对象时对齐RSP;被调用者对 RSP 所做的事情不会影响数据所在的地址,也不会复制它。

很明显,这会降低这个特定函数的效率,但是这个函数太小了,你绝对应该确保它内联到任何调用站点,此时所有这些开销都消失了。 (或者至少对较大的函数进行一次不必要的 RSP 对齐,而不是每次调用小函数一次。)

调用者必须运行至少 4vmovapd条存储指令,并且比调用站点周围的更多,特别是因为 x86-64 System V 没有保留调用的 ymm(甚至 xmm)寄存器。因此,任何其他存在于寄存器中的 FP / 向量变量或临时变量都必须在调用此函数时溢出。并且调用开销本身需要前端一些时间和一些静态代码大小。

非内联版本的每个调用站点的代码量可能与仅内联它所需的代码量相似!这意味着内联是纯粹的胜利。


推荐阅读