在内联汇编中使用特定的 zmm 寄存器

Bee*_*ope 2 x86 assembly gcc inline-assembly

我可以告诉gcc 风格的内联汇编将我的__m512i变量放入特定的 zmm寄存器中,例如zmm31

Pet*_*des 6

就像在根本没有特定寄存器约束的目标上(如 ARM)一样,使用本地寄存器变量来获得广泛的约束来为语句选择特定的寄存器asm。编译器仍然可以以其他方式进行优化,因为寄存器本地的唯一有记录的保证asm效果是针对输入/输出。

即使没有,编译器也会更喜欢asm指定的寄存器。(因此,您可以编写看起来可以工作但总体上不安全的代码,例如register int ebx asm("ebx"); return ebx;.GCC 文档使行为得到保证/面向未来,即使当前的 gcc 更喜欢使用指定的寄存器,足以在约束与指定的寄存器不兼容,请参见下文。)

不管怎样,使用 register-asm局部变量是它们唯一能保证工作的地方

#include <immintrin.h>
__m512i foo() {
    register __m512i z31 asm("zmm31") = _mm512_set1_epi32(123);
    register __m512i z30 asm("zmm30");

    asm("vmovdqa64 %1, %0  # from inline asm"
        : "=v"(z30)
        : "v"(z31)
       );
    return z30;
}
Run Code Online (Sandbox Code Playgroud)

Godbolt 编译器资源管理器上,使用 clang6.0 编译为:

    # clang -O3 -march=skylake-avx512
    vbroadcastss    .LCPI0_0(%rip), %zmm31 # zmm31 = [1.72359711E-43,1.72359711E-43,1.72359711E-43,1.72359711E-43,1.72359711E-43,1.72359711E-43,1.72359711E-43,1.72359711E-43,1.72359711E-43,1.72359711E-43,1.72359711E-43,1.72359711E-43,1.72359711E-43,1.72359711E-43,1.72359711E-43,1.72359711E-43]
    vmovdqa64       %zmm31, %zmm30        # from inline asm
    vmovaps %zmm30, %zmm0
    retq
Run Code Online (Sandbox Code Playgroud)

和 gcc8.2:

# gcc -O3 -march=skylake-avx512
foo():
    movl    $123, %eax
    vpbroadcastd    %eax, %zmm31
    vmovdqa64 %zmm31, %zmm30  # from inline asm
    vmovdqa64       %zmm30, %zmm0
    ret
Run Code Online (Sandbox Code Playgroud)

请注意"v"允许任何 EVEX 矢量寄存器 (0..31) 的约束"x",与仅允许前 16 个 不同"x"。被记录为“任何 SSE 寄存器”,但也适用于 AVX YMM 寄存器。 https://gcc.gnu.org/onlinedocs/gcc/Machine-Constraints.html

使用"x"此方法不会导致任何警告,但 gcc"x"战胜了寄存器变量声明,因此它选择了 %zmm2 和 %zmm1 (奇怪的是不是,zmm0因此需要额外的移动)。因此,register-asm 声明确实降低了我们的效率。

使用 clang 时,它仍然使用 zmm31 和 zmm30,显然违反了"x"约束,因此如果您在寄存器操作数的 XMM 或 YMM 部分上使用没有 EVEX 版本的指令,如 AVX2 vpcmpeqd ymm,ymm,ymm(与向量比较,不与掩码进行比较)。(在 GNU C 内联汇编中,单个操作数的 xmm/ymm/zmm 修饰符是什么?)。

//#ifndef __clang__
__m512i broken_with_clang() {
    register __m512i z31 asm("zmm31") = _mm512_set1_epi32(123);
    register __m512i z30 asm("zmm30") = _mm512_setzero_si512();
    // notice that gcc still inits these in zmm31 and 30, *then* copies
    // so register asm costs us efficiency.

    // AVX512 only has compares into k registers, not into YMM registers.
    asm("vpcmpeqd %t1, %t0, %t0  # from inline asm. input was %0"
        : "+x"(z30)
        : "x"(z31)
       );
    return z30;
}
//#endif
Run Code Online (Sandbox Code Playgroud)

使用 clang,我们会得到每个操作数的错误;我猜想 clang 不支持t修饰符来获取寄存器的 YMM 名称(因为即使我register ... asm()完全删除这些内容,clang6.0 也会失败。)

<source>:21:9: error: invalid operand in inline asm: 'vpcmpeqd ${1:t}, ${0:t}, ${0:t}  # from inline asm. input was $0'
    asm("vpcmpeqd %t1, %t0, %t0  # from inline asm. input was %0"
        ^
...
<source>:21:9: error: unknown token in expression
<inline asm>:1:11: note: instantiated into assembly here
        vpcmpeqd , ,   # from inline asm. input was %zmm30
Run Code Online (Sandbox Code Playgroud)

但 gcc 编译得很好:

broken_with_clang():
    movl    $123, %eax
    vpbroadcastd    %eax, %zmm31
    vpxord  %xmm30, %xmm30, %xmm30

    vmovdqa64       %zmm30, %zmm1    # extra overhead because of register asm
    vmovdqa64       %zmm31, %zmm2    # which didn't match the constraints

    vpcmpeqd %ymm2, %ymm1, %ymm1  # from inline asm. input was %zmm1

    vmovdqa64       %zmm1, %zmm0     # extra overhead because gcc didn't pick zmm0
    ret
Run Code Online (Sandbox Code Playgroud)