【问题标题】:Why do alignment restrictions change the behaviour of clang while vectorising?为什么对齐限制会在矢量化时改变 clang 的行为?
【发布时间】:2021-02-06 03:55:11
【问题描述】:

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

【问题讨论】:

  • 您是如何提供这些对齐限制的?这只是一个简单的alignas吗?我也对大会有点困惑。您标记为具有对齐限制的那个我会假设是未对齐的那个,因为前几条指令似乎计算了一个 32 位偏移对齐边界。

标签: c++ clang x86-64 memory-alignment avx


【解决方案1】:

第一种方式是无用的对齐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 System V 没有保留调用的 ymm(甚至是 xmm)寄存器。因此,任何其他存在于寄存器中的 FP / 向量变量或临时变量都必须在调用此函数时溢出。而且调用开销本身会占用前端一些时间,以及一些静态代码大小。

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

【讨论】:

    【解决方案2】:

    谁能解释一下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 不是一种便携式技术,并且经常被批评为占用过多的裸片尺寸而实用性非常有限。

    【讨论】:

    • 这基本上就是我正在做的事情,我将有 5 个实现:AVX512、AVX、SSE、Neon,什么都没有。我刚刚使用 __m256d 类型进行了对齐。但由于我的服务器是我唯一支持 AVX512 的 CPU,因此 AVX512 支持滞后。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-12-30
    • 1970-01-01
    • 1970-01-01
    • 2021-12-18
    • 2013-05-29
    • 2014-07-19
    相关资源
    最近更新 更多