__asm { ... };
据我所知,和之间的唯一区别__asm__("...");
是第一次使用mov eax, var
和第二次使用movl %0, %%eax
结尾:"=r" (var)
。还有哪些不同之处?而只是asm
呢?
4 回答
MSVC 内联汇编和 GNU C 内联汇编之间存在巨大差异。GCC 语法旨在优化输出而不会浪费指令,用于包装单个指令或其他东西。MSVC 语法被设计得相当简单,但是如果没有延迟和额外的指令,就不可能使用 AFAICT 来为您的输入和输出在内存中往返。
如果您出于性能原因使用内联汇编,这使得 MSVC 内联汇编仅在您完全用汇编编写整个循环时才可行,而不是用于将短序列包装在内联函数中。下面的示例(idiv
使用函数包装)是 MSVC 不擅长的:~8 个额外的存储/加载指令。
MSVC 内联汇编(由 MSVC 使用,可能还有 icc,也可能在某些商业编译器中可用):
- 查看您的 asm 以确定您的代码在哪个寄存器上执行。
- 只能通过内存传输数据。例如,存在于寄存器中的数据由编译器存储以准备您
mov ecx, shift_count
的 . 因此,使用编译器不会为您生成的单个 asm 指令涉及在进出内存的过程中往返。 - 对初学者更友好,但通常无法避免将数据输入/输出的开销。即使除了语法限制之外,当前版本的 MSVC 中的优化器也不擅长围绕内联 asm 块进行优化。
GNU C 内联 asm不是学习 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
for (int64_t)a / (int32_t)b
,可能是因为如果结果不适合 32 位寄存器,指令就会出错。因此,与这个关于从 one 获取商和余数的问答div
不同,这是内联汇编的一个用例。(除非有办法通知编译器结果适合,所以 idiv 不会出错。)
我们将使用调用约定将一些 args 放入寄存器中(hi
甚至在正确的寄存器中),以显示更接近内联这样一个小函数时所看到的情况。
MSVC
使用 inline-asm 时要小心 register-arg 调用约定。显然,inline-asm 支持的设计/实现非常糟糕,以至于编译器可能不会在内联 asm 周围保存/恢复 arg 寄存器,如果这些 args 未在 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
更新:显然在or中留下一个值,然后从非 void 函数(没有 a )edx:eax
的末尾脱落,即使内联也是如此return
。asm
我认为这仅在语句后没有代码时才有效。请参阅是否 __asm{}; 返回eax的值? 这避免了输出的存储/重新加载(至少对于quotient
),但我们不能对输入做任何事情。在带有堆栈参数的非内联函数中,它们已经在内存中,但在这个用例中,我们正在编写一个可以有用内联的小函数。
/O2
在 rextester上使用 MSVC 19.00.23026 编译(使用 amain()
找到 exe 的目录并将编译器的 asm 输出转储到 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块之前从堆栈加载到寄存器中。
这个函数实际上比普通的一切堆栈 ABI更糟糕。_vectorcall
在寄存器中有两个输入,它将它们存储到内存中,因此内联 asm 可以从命名变量中加载它们。如果这是内联的,那么更多的参数可能会在 regs 中,并且必须将它们全部存储起来,因此 asm 将具有内存操作数!因此,与 gcc 不同的是,我们从内联中并没有获得太多收益。
在 asm 块内执行*premainder = tmp
意味着更多的代码用 asm 编写,但确实避免了其余部分的完全脑死的存储/加载/存储路径。这将指令计数减少了 2 条,减少到 11 条(不包括ret
)。
我试图从 MSVC 中获得最好的代码,而不是“使用错误”并创建一个稻草人论点。但是 AFAICT 包装非常短的序列是可怕的。 大概有一个 64/32 -> 32 除法的内在函数,它允许编译器为这种特殊情况生成好的代码,所以在 MSVC 上为此使用内联 asm 的整个前提可能是一个稻草人参数。但它确实向您展示了内在函数比 MSVC 的内联汇编要好得多。
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 从堆栈中弹出,这就是ret 8
MSVC 输出中的内容。)
// 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;
}
用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 个用于输出。)
您使用哪一个取决于您的编译器。这不像 C 语言那样标准。
asm
与__asm__
海湾合作委员会
asm
不适用于-std=c99
,您有两种选择:
- 采用
__asm__
- 采用
-std=gnu99
更多细节:错误:'asm' undeclared(在此函数中首次使用)
__asm
与__asm__
海湾合作委员会
我找不到记录在哪里(特别是在https://gcc.gnu.org/onlinedocs/gcc-7.2.0/gcc/Alternate-Keywords.html#Alternate-Keywords__asm
中没有提到),但是从GCC 8.1 源他们完全相同:
{ "__asm", RID_ASM, 0 },
{ "__asm__", RID_ASM, 0 },
所以我只会使用__asm__
记录在案的内容。
使用 gcc 编译器,差别不大。asm
or __asm
or or__asm__
相同,它们只是用来避免冲突命名空间的目的(有用户定义的函数,命名为 asm 等)