__asm { ... };
和 __asm__("...");
的唯一区别是前者使用 mov eax, var
,而后者使用 movl %0, %%eax
,并在末尾加上 :"=r" (var)
。除此之外还有哪些区别?asm
又如何呢?__asm { ... };
和 __asm__("...");
的唯一区别是前者使用 mov eax, var
,而后者使用 movl %0, %%eax
,并在末尾加上 :"=r" (var)
。除此之外还有哪些区别?asm
又如何呢?MSVC内联汇编(被MSVC和可能也被一些商业编译器使用):
mov ecx, shift_count
等指令之前,将寄存器中的数据存储起来。因此,使用单个编译器无法为您生成的汇编指令需要在输入和输出时通过内存进行往返传输。GNU C内联汇编不是学习汇编的好方法。你必须非常了解汇编,以便告诉编译器有关您的代码的信息。同时,你还必须知道编译器需要什么信息。该回答还提供了其他内联汇编指南和问答链接。x86标签维基包含许多有关汇编的好内容,但只针对GNU内联汇编提供链接。(该答案中的内容也适用于非x86平台上的GNU内联汇编。)
gcc、clang、icc和可能一些实现GNU C的商业编译器使用GNU C内联汇编语法:
You have to tell the compiler what you clobber. Failure to do this will lead to breakage of surrounding code in non-obvious hard-to-debug ways.
Powerful but hard to read, learn, and use syntax for telling the compiler how to supply inputs, and where to find outputs. e.g. "c" (shift_count)
will get the compiler to put the shift_count
variable into ecx
before your inline asm runs.
extra clunky for large blocks of code, because the asm has to be inside a string constant. So you typically need
"insn %[inputvar], %%reg\n\t" // comment
"insn2 %%reg, %[outputvar]\n\t"
very unforgiving / harder, but allows lower overhead esp. for wrapping single instructions. (wrapping single instructions was the original design intent, which is why you have to specially tell the compiler about early clobbers to stop it from using the same register for an input and output if that's a problem.)
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不会出错。)
我们将使用调用约定,将一些参数放入寄存器中(其中hi
甚至在正确的寄存器中),以显示更接近内联类似此类小函数的情况。
在使用内联汇编时,请注意寄存器参数调用约定。显然,内联汇编支持非常糟糕的设计/实现,如果这些参数没有在内联汇编中使用,编译器可能不会保存/恢复内联汇编周围的参数寄存器。感谢@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
)的情况下是支持的,即使是内联的。 我认为只有在asm
语句之后没有代码的情况下才有效。请参见Does __asm{}; return the value of eax?这可以避免输出的存储/重新加载(至少对于quotient
)。但是,我们对输入无能为力。 在具有堆栈参数的非内联函数中,它们已经在内存中,但在这种用例中,我们正在编写一个可以有用地内联的小型函数。
使用MSVC 19.00.23026编译 /O2
(在rextester上查看)(其中main()
函数查找exe文件的目录并将编译器的汇编输出转储到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
当内联div64时,Gcc的表现甚至比这里展示的输出要好,因为它通常可以安排前面的代码在第一次生成64位整数时就在edx:eax中生成。
我无法让gcc编译32位矢量调用ABI。Clang可以,但在使用“rm”约束的内联asm上表现很差(在godbolt链接上尝试一下:它会通过内存反弹函数参数,而不是使用约束中的寄存器选项)。64位MS调用约定接近于32位矢量调用,前两个参数在edx、ecx中。区别在于,在使用堆栈之前,还有2个参数在寄存器中(以及被调用者不会从堆栈中弹出参数,这就是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
## 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次输出的加载/存储。)
__stdcall
编译了该函数,但__cdecl
同样适用。不要使用__vectorcall
或__fastcall
,它们只会使内联汇编变得更糟。请注意,输出与GCC的几乎完全相同,除了需要将参数从堆栈显式加载到寄存器中。这是MSVC内联汇编的硬性限制,完全无法避免,必然导致次优代码。 - Cody Gray使用哪个取决于您的编译器。这不像C语言那样是标准的。
asm
与__asm__
在GCC中的区别
asm
在使用-std=c99
选项时将无法工作,您有两个替代方案:
__asm__
-std=gnu99
更多详情请参考:error: ‘asm’ undeclared (first use in this function)
asm
与__asm__
在GCC中的区别
我无法找到__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__
。
__asm { ... };
(有大括号,不是圆括号),因此它绝对不是GNU C内联汇编。Clang和MSVC支持MSVC风格的内联汇编,但gcc不支持。 - Peter Cordesgcc
之外,您能否还将适当的Microsoft编译器标签添加到问题中?谢天谢地,我不知道它是哪一个 :-) - Ciro Santilli OurBigBook.com使用gcc编译器,这并没有太大的区别。 asm
、__asm
或 __asm__
都是一样的,它们只是用于避免命名空间冲突的目的(例如,有用户定义的名为asm的函数等)。