为什么对齐限制会在向量化时改变 clang 的行为?

Why do alignment restrictions change the behaviour of clang while vectorising?

谁能解释一下 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 的。它不仅适用于乘法,而且适用于任何计算。一个版本比另一个好吗?

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

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

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

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

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

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

Can anyone explain the behaviour of clang?

假设Array4Complex64

using Array4Complex64 = double __attribute__ ((vector_size (64)));

这是一个512位类型,使用avx512效率最高:

f1(double __vector(8), double __vector(8)):                           # @f1(double __vector(8), double __vector(8))
        vmulpd  zmm0, zmm0, zmm1
        ret

然而,avx512 不是一种便携式技术,并且经常被批评占用过多的芯片尺寸且实用性非常有限。