在 Clang/LLVM x86-64 内联汇编中,我怎么说我破坏了 x87/media 状态?

In Clang/LLVM x86-64 inline assembly, how do I say I clobbered the x87/media state?

我正在编写一些可能会影响浮点和媒体(SSE、MMX 等)状态的 x86-64 内联程序集,但我不想自己保存和恢复状态。 Clang/LLVM 有 clobber constraint 吗?

(我不太熟悉 x86-64 架构或内联汇编,所以很难知道要搜索什么。如果这是一个 XY 问题,请提供更多详细信息:我正在研究一个简单的Rust 中的协程库。当我们切换任务时,我们需要存储旧的 CPU 状态并加载新状态,我想编写尽可能少的程序集。我的猜测是让编译器来处理保存和恢复状态是最简单的方法。)

如果你的协程看起来像一个不透明的(非内联)函数调用,编译器将已经假设 FP 状态被破坏(除了像 MXCSR 和 x87 这样的控制寄存器控制字(舍入模式)),因为所有 FP regs 在正常函数调用约定中都被调用破坏。

除了 Windows,其中 xmm6..15 是调用保留的。


另外请注意,如果您将 call 放入内联 asm 中,则无法告诉编译器您的 asm 破坏了红色区域 (128 字节低于 x86-64 System V ABI 中的 RSP)。您可以使用 -mno-redzone 编译该文件或在 call 之前使用 add rsp, -128 以跳过属于编译器生成代码的红色区域。


要在 FP 状态上声明 clobber,您必须分别命名所有寄存器。

"xmm0", "xmm1", ..., "xmm15"(破坏 xmm0 算作破坏 ymm0/zmm0)。

为了更好地衡量,您还应该命名 "mm0", ..., "mm7" (MMX),以防您的代码使用 MMX 内在函数内联到某些遗留代码中。

为了破坏 x87 堆栈,"st" 是您在破坏列表中引用 st(0) 的方式。其余寄存器具有 GAS 语法的正常名称,"st(1)", ..., "st(7)". You never know, it is possible to compile withclang -mfpmath=387, or to use 387 vialong double`.

(希望没有代码在 64 位模式下同时使用 -mfpmath=387 MMX 内在函数;以下测试用例在 gcc 中看起来略有问题例。)

#include <immintrin.h>
float gvar;
int testclobber(float f, char *p)
{
    int arg1 = 1, arg2 = 2;

    f += gvar;  // with -mno-sse, this will be in an x87 register
    __m64 mmx_var = *(const __m64*)p;             // MMX
    mmx_var = _mm_unpacklo_pi8(mmx_var, mmx_var);

    // x86-64 System V calling convention
    unsigned long long retval;
    asm volatile ("add $-128, %%rsp \n\t"   // skip red zone.  -128 fits in an imm8
                  "call whatever \n\t"
                  "sub $-128, %%rsp  \n\t"
                 // FIXME should probably align the stack in here somewhere

                 : "=a"(retval)            // returns in RAX
                 : "D" (arg1), "S" (arg2)  // input args in registers

                 : "rcx", "rdx", "r8", "r9", "r10", "r11"  // call-clobbered integer regs
                  // call clobbered FP regs, *NOT* including MXCSR
                  , "mm0", "mm1", "mm2", "mm3", "mm4", "mm5", "mm6", "mm7"           // MMX
                  , "st", "st(1)", "st(2)", "st(3)", "st(4)", "st(5)", "st(6)", "st(7)"  // x87
                  // SSE/AVX: clobbering any results in a redundant vzeroupper with gcc?
                  , "xmm0", "xmm1", "xmm2", "xmm3", "xmm4", "xmm5", "xmm6", "xmm7"
                  , "xmm8", "xmm9", "xmm10", "xmm11", "xmm12", "xmm13", "xmm14", "xmm15"
                 #ifdef __AVX512F__
                  , "zmm16", "zmm17", "zmm18", "zmm19", "zmm20", "zmm21", "zmm22", "zmm23"
                  , "zmm24", "zmm25", "zmm26", "zmm27", "zmm28", "zmm29", "zmm30", "zmm31"
                  , "k0", "k1", "k2", "k3", "k4", "k5", "k6", "k7"
                 #endif
                 #ifdef __MPX__
                , "bnd0", "bnd1", "bnd2", "bnd3"
                #endif

                , "memory"  // reads/writes of globals and pointed-to data can't reorder across the asm (at compile time; runtime StoreLoad reordering is still a thing)
         );

    // Use the MMX var after the asm: compiler has to spill/reload the reg it was in
    *(__m64*)p = mmx_var;
    _mm_empty();   // emms

    gvar = f;  // memory clobber prevents hoisting this ahead of the asm.

    return retval;
}

源码+汇编on the Godbolt compiler explorer

通过评论其中一行 clobbers,我们可以看到 spill-reload 在 asm 中消失了。例如评论 x87 st .. st(7) clobbers 使得代码在 st0 中留下 f + gvar,在调用后仅 fst dword [gvar]

类似地,注释 mm0 行让 gcc 和 clang 在 call 中的 mm0 中保留 mmx_varABI 要求 FPU 处于 x87 模式,而不是 MMX,在 call / ret,这还不够。 编译器将 spill/reload 围绕 asm,但它不会为我们插入 emms但是出于同样的原因,如果函数使用 MMX 调用您的协同例程而不先执行 _mm_empty() 将是一个错误,所以也许这不是一个真正的问题。

我还没有尝试过 __m256 变量来查看它是否在 asm 之前插入 vzeroupper,以避免可能的 SSE/AVX 减速。

如果我们注释 xmm8..15 行,我们会看到 float 未使用 x87 的版本保留在 xmm8 中,因为现在它认为它有一些非被破坏的 xmm regs。 如果我们评论两组行,它假定 xmm0 存在于整个 asm 中,因此这可以作为对破坏者的测试。


asm 输出,所有 clobbers 就位

它 saves/restores RBX(在 asm 语句中保存指针 arg),恰好将堆栈重新对齐 16。这是从内联 asm 使用 call 的另一个问题:我不要认为 RSP 的对齐是有保证的。

# from clang7.0 -march=skylake-avx512 -mmpx
testclobber:                            # @testclobber
    push    rbx
    vaddss  xmm0, xmm0, dword ptr [rip + gvar]
    vmovss  dword ptr [rsp - 12], xmm0 # 4-byte Spill   (because of xmm0..15 clobber)
    mov     rbx, rdi                    # save pointer for after asm
    movq    mm0, qword ptr [rdi]
    punpcklbw       mm0, mm0        # mm0 = mm0[0,0,1,1,2,2,3,3]
    movq    qword ptr [rsp - 8], mm0 # 8-byte Spill    (because of mm0..7 clobber)
    mov     edi, 1
    mov     esi, 2
    add     rsp, -128
    call    whatever
    sub     rsp, -128

    movq    mm0, qword ptr [rsp - 8] # 8-byte Reload
    movq    qword ptr [rbx], mm0
    emms                                     # note this didn't happen before call
    vmovss  xmm0, dword ptr [rsp - 12] # 4-byte Reload
    vmovss  dword ptr [rip + gvar], xmm0
    pop     rbx
    ret

请注意,由于 asm 语句中的 "memory" 破坏,*pgvar 在 asm 之前读取,但在之后写入。否则,优化器可能会降低负载或提升存储,因此 asm 语句中没有局部变量。但是现在优化器需要假设 asm 语句本身可能会读取 gvar 的旧值 and/or 修改它。 (并假设 p 指向也可以通过某种方式全局访问的内存,因为我们没有使用 __restrict。)