MSVC 内联汇编和 GNU C 内联汇编之间存在巨大差异。 GCC 语法旨在优化输出而不会浪费指令,用于包装单个指令或其他东西。 MSVC 语法被设计得相当简单,但是如果没有延迟和额外的指令,就无法使用 AFAICT,因为您的输入和输出需要在内存中往返。
如果您出于性能原因使用内联 asm,这使得 MSVC 内联 asm 仅在您完全用 asm 编写整个循环时才可行,而不是用于将短序列包装在内联函数中。下面的示例(用函数包装 idiv)是 MSVC 不擅长的事情:~8 个额外的存储/加载指令。
MSVC 内联汇编(MSVC 使用,可能还有 icc,也可能在一些商业编译器中可用):
- 查看您的 asm 以确定您的代码在哪个寄存器上运行。
- 只能通过内存传输数据。例如,在寄存器中存在的数据由编译器存储以准备您的
mov ecx, shift_count。因此,使用编译器不会为您生成的单个 asm 指令涉及在进出内存的过程中往返。
-
对初学者更友好,但通常无法避免输入/输出数据的开销。即使除了语法限制之外,当前版本的 MSVC 中的优化器也不擅长围绕内联 asm 块进行优化。
GNU C 内联汇编is not a good way to learn asm。您必须非常了解 asm,以便您可以告诉编译器您的代码。你必须了解编译器需要知道什么。该答案还具有其他内联汇编指南和问答的链接。 x86 标签 wiki 通常有很多关于 asm 的好东西,但只是指向 GNU 内联 asm 的链接。 (该答案中的内容也适用于非 x86 平台上的 GNU 内联汇编。)
gcc、clang、icc 以及一些实现 GNU C 的商业编译器使用 GNU C 内联 asm 语法:
- 你必须告诉编译器你破坏了什么。不这样做会导致周围代码以非显而易见的难以调试的方式被破坏。
- 功能强大但难以阅读、学习和使用语法来告诉编译器如何提供输入以及在哪里找到输出。例如
"c" (shift_count) 将让编译器在您的内联汇编运行之前将 shift_count 变量放入 ecx。
-
对于大块代码来说特别笨重,因为 asm 必须在字符串常量内。所以你通常需要
"insn %[inputvar], %%reg\n\t" // comment
"insn2 %%reg, %[outputvar]\n\t"
非常无情/更难,但允许较低的开销,尤其是。用于包装单个指令。 (包装单个指令是最初的设计意图,这就是为什么你必须特别告诉编译器早期的clobbers 以阻止它在出现问题时使用相同的寄存器进行输入和输出。)
示例:全角整数除法 (div)
在 32 位 CPU 上,将 64 位整数除以 32 位整数,或进行全乘 (32x32->64),可以从内联汇编中受益。 gcc 和 clang 没有利用 idiv 代替 (int64_t)a / (int32_t)b,可能是因为如果结果不适合 32 位寄存器,指令就会出错。所以不像this Q&A about getting quotient and remainder from one div,这是一个内联汇编的用例。 (除非有办法通知编译器结果适合,所以 idiv 不会出错。)
我们将使用调用约定将一些 args 放入寄存器中(hi 甚至在 right 寄存器中),以显示更接近于内联 tiny像这样的函数。
MSVC
在使用 inline-asm 时要小心 register-arg 调用约定。显然,inline-asm 支持的设计/实现非常糟糕,以至于the compiler might not save/restore arg registers around the inline asm, if those args aren't used in the inline asm。感谢@RossRidge 指出这一点。
// MSVC. Be careful with _vectorcall & inline-asm: see above
// we could return a struct, but that would complicate things
int _vectorcall div64(int hi, int lo, int divisor, int *premainder) {
int quotient, tmp;
__asm {
mov edx, hi;
mov eax, lo;
idiv divisor
mov quotient, eax
mov tmp, edx;
// mov ecx, premainder // Or this I guess?
// mov [ecx], edx
}
*premainder = tmp;
return quotient; // or omit the return with a value in eax
}
更新:显然在eax 或edx:eax 中留下了一个值,然后从非空函数(没有return)is supported, even when inlining 的末尾脱落。。我认为这仅在asm 语句之后没有代码时才有效。请参阅Does __asm{}; return the value of eax? 这避免了输出的存储/重新加载(至少对于quotient),但我们对输入无能为力。在带有堆栈参数的非内联函数中,它们已经在内存中,但在这个用例中,我们正在编写一个可以有用内联的小函数。
使用 MSVC 19.00.23026 /O2 on rextester 编译(带有用于查找 exe 目录的 main() 和 dumps the compiler's asm output to stdout)。
## My added comments use. ##
; ... define some symbolic constants for stack offsets of parameters
; 48 : int ABI div64(int hi, int lo, int divisor, int *premainder) {
sub esp, 16 ; 00000010H
mov DWORD PTR _lo$[esp+16], edx ## these symbolic constants match up with the names of the stack args and locals
mov DWORD PTR _hi$[esp+16], ecx
## start of __asm {
mov edx, DWORD PTR _hi$[esp+16]
mov eax, DWORD PTR _lo$[esp+16]
idiv DWORD PTR _divisor$[esp+12]
mov DWORD PTR _quotient$[esp+16], eax ## store to a local temporary, not *premainder
mov DWORD PTR _tmp$[esp+16], edx
## end of __asm block
mov ecx, DWORD PTR _premainder$[esp+12]
mov eax, DWORD PTR _tmp$[esp+16]
mov DWORD PTR [ecx], eax ## I guess we should have done this inside the inline asm so this would suck slightly less
mov eax, DWORD PTR _quotient$[esp+16] ## but this one is unavoidable
add esp, 16 ; 00000010H
ret 8
有大量额外的 mov 指令,编译器甚至无法优化其中的任何一条。我想它可能会看到并理解内联汇编中的mov tmp, edx,并将其作为premainder 的存储。但我猜这需要将premainder 从堆栈加载到内联 asm 块之前的寄存器中。
使用_vectorcall 的这个函数实际上更糟糕,而不是使用普通的everything-on-the-stack ABI。在寄存器中有两个输入,它将它们存储到内存中,因此内联 asm 可以从命名变量中加载它们。如果这是内联的,则更多的参数可能会在 regs 中,并且必须将它们全部存储起来,因此 asm 将具有内存操作数!因此,与 gcc 不同的是,内联它并没有太多好处。
在 asm 块内执行 *premainder = tmp 意味着更多的代码是用 asm 编写的,但确实避免了其余部分的完全脑死的存储/加载/存储路径。这将指令总数减少了 2 条,降至 11 条(不包括 ret)。
我试图从 MSVC 中获得最好的代码,而不是“使用错误”并创建一个稻草人论据。但是 AFAICT 包装非常短的序列是可怕的。 大概有一个 64/32 -> 32 除法的内在函数允许编译器为这种特殊情况生成好的代码,所以在 MSVC 上为此使用内联 asm 的整个前提可能是一个稻草人参数强>。但它确实向您表明,对于 MSVC,内在函数比内联 asm 好得多。
GNU C (gcc/clang/icc)
当内联 div64 时,Gcc 甚至比此处显示的输出更好,因为它通常可以安排前面的代码首先在 edx:eax 中生成 64 位整数。
我无法让 gcc 为 32 位 vectorcall ABI 进行编译。 Clang 可以,但它在带有"rm" 约束的内联 asm 上很糟糕(在 Godbolt 链接上尝试:它通过内存反弹函数 arg 而不是在约束中使用 register 选项)。 64 位 MS 调用约定接近 32 位向量调用,前两个参数在 edx、ecx 中。不同之处在于在使用堆栈之前还有 2 个参数进入 regs(并且被调用者不会将 args 从堆栈中弹出,这就是 MSVC 输出中的 ret 8 的含义。)
// GNU C
// change everything to int64_t to do 128b/64b -> 64b division
// MSVC doesn't do x86-64 inline asm, so we'll use 32bit to be comparable
int div64(int lo, int hi, int *premainder, int divisor) {
int quotient, rem;
asm ("idivl %[divsrc]"
: "=a" (quotient), "=d" (rem) // a means eax, d means edx
: "d" (hi), "a" (lo),
[divsrc] "rm" (divisor) // Could have just used %0 instead of naming divsrc
// note the "rm" to allow the src to be in a register or not, whatever gcc chooses.
// "rmi" would also allow an immediate, but unlike adc, idiv doesn't have an immediate form
: // no clobbers
);
*premainder = rem;
return quotient;
}
compiled with gcc -m64 -O3 -mabi=ms -fverbose-asm。使用 -m32,您只需获得 3 个负载、idiv 和一个商店,正如您可以从更改该 Godbolt 链接中的内容中看到的那样。
mov eax, ecx # lo, lo
idivl r9d # divisor
mov DWORD PTR [r8], edx # *premainder_7(D), rem
ret
对于 32 位向量调用,gcc 会做类似的事情
## Not real compiler output, but probably similar to what you'd get
mov eax, ecx # lo, lo
mov ecx, [esp+12] # premainder
idivl [esp+16] # divisor
mov DWORD PTR [ecx], edx # *premainder_7(D), rem
ret 8
MSVC 使用 13 条指令(不包括 ret),而 gcc 的 4 条。正如我所说,使用内联,它可能只编译为一条,而 MSVC 仍可能使用 9 条。(它不需要保留堆栈空间或加载premainder;我假设它仍然必须存储3个输入中的大约2个。然后它在asm中重新加载它们,运行idiv,存储两个输出,然后在asm之外重新加载它们。所以这是4加载/存储用于输入,另外 4 个用于输出。)