在 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 with
clang -mfpmath=387, or to use 387 via
long 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_var
。 ABI 要求 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"
破坏,*p
和 gvar
在 asm 之前读取,但在之后写入。否则,优化器可能会降低负载或提升存储,因此 asm
语句中没有局部变量。但是现在优化器需要假设 asm
语句本身可能会读取 gvar
的旧值 and/or 修改它。 (并假设 p
指向也可以通过某种方式全局访问的内存,因为我们没有使用 __restrict
。)
我正在编写一些可能会影响浮点和媒体(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)".
clang -mfpmath=387, or to use 387 via
long 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_var
。 ABI 要求 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"
破坏,*p
和 gvar
在 asm 之前读取,但在之后写入。否则,优化器可能会降低负载或提升存储,因此 asm
语句中没有局部变量。但是现在优化器需要假设 asm
语句本身可能会读取 gvar
的旧值 and/or 修改它。 (并假设 p
指向也可以通过某种方式全局访问的内存,因为我们没有使用 __restrict
。)