‘asm’,’__ asm’和’__asm__’有什么区别?
据我所知, __asm { ... };
之间的唯一区别__asm { ... };
和__asm__("...");
是第一个使用mov eax, var
,第二个使用movl %0, %%eax
使用:"=r" (var)
。 还有什么其他差异? 那么asm
呢?
您使用哪一个取决于您的编译器。 这不像C语言那样标准。
MSVC内联asm和GNU C内联asm之间存在巨大差异。 GCC语法设计用于最佳输出而不会浪费指令,用于包装单个指令或其他内容。 MSVC语法设计得相当简单,但AFAICT如果没有延迟和额外的指令通过内存进行输入和输出,就不可能使用它。
如果出于性能原因使用内联asm,这使得MSVC内联asm只有在完全用asm编写完整循环时才可行,而不是用于在内联函数中包装短序列。 下面的例子(用函数包装idiv
)是MSVC不好的东西:~8个额外的存储/加载指令。
MSVC内联asm(由MSVC和可能的icc使用,也可能在某些商业编译器中使用):
- 看看你的asm,找出你的代码所处的寄存器。
- 只能通过内存传输数据。 寄存器中存在的数据由编译器存储,以便为您的
mov ecx, shift_count
做准备。 因此,使用编译器不会为您生成的单个asm指令,包括在路上和路上的往返内存。 - 更适合初学者,但通常无法避免数据输入/输出 。 即使除了语法限制之外,当前版本的MSVC中的优化器也不擅长围绕内联asm块进行优化。
GNU C inline asm 不是学习asm的好方法 。 您必须非常了解asm,以便您可以告诉编译器您的代码。 而且你必须了解编译器需要知道什么。 该答案还与其他inline-asm指南和Q&A有关。 对于asm, x86标签wiki有很多好东西,但只是GNU内联asm的链接。 (该答案中的内容也适用于非x86平台上的GNU内联asm。)
GNU C inline asm语法由gcc,clang,icc和一些实现GNU C的商业编译器使用:
- 你必须告诉编译器你破坏了什么。 如果不这样做,将导致以非显而易见的难以调试的方式破坏周围的代码。
- function强大但难以阅读,学习和使用语法来告诉编译器如何提供输入,以及在何处查找输出。 例如,
"c" (shift_count)
将使编译器在你的内联asm运行之前将shift_count
变量放入ecx
。 -
因为asm必须在一个字符串常量内,所以对于大块代码来说是额外的笨重。 所以你通常需要
"insn %[inputvar], %%reg\n\t" // comment "insn2 %%reg, %[outputvar]\n\t"
-
非常无情/更难,但允许更低的开销esp。 用于包装单个指令 。 (包装单个指令是原始的设计意图,这就是为什么你必须特别告诉编译器有关早期的clobbers来阻止它使用相同的寄存器进行输入和输出,如果这是一个问题。)
示例:全宽整数除法( div
)
在32位CPU上,将64位整数除以32位整数,或者进行全乘(32×32-> 64),可以从内联asm中受益。 gcc和clang没有利用idiv
(int64_t)a / (int32_t)b
,可能是因为如果结果不适合32位寄存器,指令就会出错。 所以不像这个关于从一个div
获得商和余数的Q&A ,这是内联asm的用例。 (除非有办法告知编译器结果是否合适,所以idiv不会出错。)
我们将使用调用约定将一些args放在寄存器中(即使在正确的寄存器中也是hi
),以显示一个更接近你在内联这样一个小函数时看到的情况。
MSVC
使用inline-asm时要注意register-arg调用约定。 显然,如果在内联asm中没有使用这些args,则内联asm支持的设计/实现非常糟糕,以至于编译器可能无法在内联asm周围保存/恢复arg寄存器 。 感谢@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
保留一个值,然后在非void函数(不return
)的末尾掉落,即使在内联时也是如此 。 我假设只有在asm
语句之后没有代码时才有效。 这避免了输出的存储/重新加载(至少对于quotient
),但我们无法对输入做任何事情。 在具有堆栈参数的非内联函数中,它们已经在内存中,但在这个用例中,我们正在编写一个可以有用内联的小函数。
在rextester上使用MSVC 19.00.23026 /O2
编译 (使用main()
查找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指令,编译器甚至没有接近优化任何一个。 我想也许它会看到并理解内联asm中的mov tmp, edx
,并将它作为premainder
的商店。 但是,我想这需要premainder
联asm块之前将堆栈中的premainder
加载到寄存器中。
对于_vectorcall
这个函数实际上比使用正常的堆栈ABI 更糟糕 。 在寄存器中有两个输入,它将它们存储到内存中,因此内联asm可以从命名变量加载它们。 如果这是内联的,那么更多的参数可能会出现在regs中,而且必须将它们全部存储起来,所以asm会有内存操作数! 因此,与gcc不同,我们并没有从内联中获得太多收益。
在asm块中执行*premainder = tmp
意味着在asm中编写更多代码,但确实避免了余数的完全braindead存储/加载/存储路径。 这将指令数量减少了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可以,但它在内联asm中有"rm"
约束(在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; }
使用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个输出。)
使用gcc编译器,它没有太大的区别。 asm
或__asm
或__asm__
是相同的,它们只是用来避免冲突命名空间的目的(有用户定义的函数,名称为asm等)
海湾合作委员会中的asm
vs __asm__
asm
不能与-std=c99
,你有两种选择:
- 使用
__asm__
- 使用
-std=gnu99
更多细节: 错误:’asm’未声明(首次使用此function)
GCC中__asm
vs __asm__
我找不到记录__asm
位置(特别是未在https://gcc.gnu.org/onlinedocs/gcc-7.2.0/gcc/Alternate-Keywords.html#Alternate-Keywords中提及),但是来自GCC 8.1来源它们完全一样:
{ "__asm", RID_ASM, 0 }, { "__asm__", RID_ASM, 0 },
所以我只想使用__asm__
,这是记录在案的。