'asm'、'__asm'和'__asm__'有什么区别?

57
据我所知,__asm { ... };__asm__("..."); 的唯一区别是前者使用 mov eax, var,而后者使用 movl %0, %%eax,并在末尾加上 :"=r" (var)。除此之外还有哪些区别?asm 又如何呢?

根据此处链接:https://msdn.microsoft.com/zh-cn/library/45yd4tzz(v=vs.120).aspx,@kennytm中的*_asm是__asm的同义词。 - smwikipedia
7
你应该选择彼得的答案,它更出色。 - Evan Carroll
4个回答

57
MSVC内联汇编和GNU C内联汇编之间存在巨大的差异。GCC语法旨在实现最佳输出,避免浪费指令,用于包装单个指令或其他内容。MSVC语法旨在保持相对简单,但据我所知,在不经过内存回路的情况下使用是不可能的,这会导致延迟和额外的指令。
(MSVC __asm{ ... } 语法也被 clang -fasm-blocks 支持,唯一的区别是 MSVC 支持将值留在 EAX 中,并从非 void 函数的末尾掉落; clang -fasm-blocks 不支持。 预计 clang-cl 也不支持。)
如果您正在使用内联汇编来提高性能,则只有在完全使用汇编编写整个循环时,MSVC内联汇编才是可行的,而不是用于包装短序列的内联函数。下面的示例(使用函数包装 idiv)就是 MSVC 不擅长的事情:多出约8个存储/加载指令。

MSVC内联汇编(被MSVC和可能也被一些商业编译器使用):

  • 查看您的汇编代码以确定您的代码使用了哪些寄存器。
  • 只能通过内存传输数据。编译器将在您使用mov ecx, shift_count等指令之前,将寄存器中的数据存储起来。因此,使用单个编译器无法为您生成的汇编指令需要在输入和输出时通过内存进行往返传输。
  • 更适合初学者,但通常无法避免获取数据的开销。除了语法限制外,当前版本的MSVC优化器在内联汇编块周围的优化方面也不够好。

GNU C内联汇编不是学习汇编的好方法。你必须非常了解汇编,以便告诉编译器有关您的代码的信息。同时,你还必须知道编译器需要什么信息。该回答还提供了其他内联汇编指南和问答链接。标签维基包含许多有关汇编的好内容,但只针对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甚至在正确的寄存器中),以显示更接近内联类似此类小函数的情况。


MSVC

在使用内联汇编时,请注意寄存器参数调用约定。显然,内联汇编支持非常糟糕的设计/实现,如果这些参数没有在内联汇编中使用,编译器可能不会保存/恢复内联汇编周围的参数寄存器。感谢@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
}
更新:显然,在将值保留在eaxedx: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

有大量多余的mov指令,编译器甚至没有优化掉任何一个。我以为它会看到并理解内联汇编中的mov tmp, edx,并将其作为对premainder的存储。但这需要在内联汇编块之前将premainder从堆栈加载到寄存器中。
使用_vectorcall比普通的everything-on-the-stack ABI更糟糕。对于两个寄存器中的输入,它将它们存储到内存中,以便内联汇编可以从命名变量中加载它们。如果这被内联,更多的参数可能会位于寄存器中,因此它必须将它们全部存储,因此asm将具有内存操作数!因此,与gcc不同,我们无法从中获得很多收益。
在asm块内执行*premainder = tmp意味着在asm中编写更多代码,但确实避免了完全愚蠢的store / load / store路径。这将总指令数减少2条,降低到11条(不包括ret)。
我试图从MSVC中获得最好的代码,而不是“错误使用”并制造一个草人论点。但据我所知,它对于包装非常短的序列非常糟糕。可能有一个内在函数用于64/32 -> 32除法,允许编译器为这种特定情况生成良好的代码,因此在MSVC上使用内联汇编的整个前提可能是一个草人论点。但它确实表明,对于MSVC来说,内部函数比内联汇编要好得多。

GNU C (gcc/clang/icc)

当内联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

对于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次输出的加载/存储。)


1
我会说微软的内联汇编不太宽容。它从来不是特别可靠的,而且新的编译器版本是否会破坏你的代码总是一种赌博。显然,对于微软来说,这也是一个巨大的维护噩梦,他们试图确保新的编译器版本不会破坏现有的代码,这就是为什么他们放弃了对其最近支持的架构(IA-64、AMD64、ARM)的支持。特别是你的MSVC示例很危险,因为它将寄存器调用约定与内联汇编混合使用:https://msdn.microsoft.com/en-us/library/k1a8ss06.aspx。 - Ross Ridge
1
@PeterCordes 嗯,对于你的特定示例,它应该可以工作。问题存在的事实表明,Microsoft编译器中的内联汇编实现非常粗糙。我认为编译器甚至不会警告您正在使用它需要用于其他用途的寄存器。 - Ross Ridge
2
说实话,这是我在MSVC中使用内联汇编编写函数的方式。底部显示了反汇编结果。我使用__stdcall编译了该函数,但__cdecl同样适用。不要使用__vectorcall__fastcall,它们只会使内联汇编变得更糟。请注意,输出与GCC的几乎完全相同,除了需要将参数从堆栈显式加载到寄存器中。这是MSVC内联汇编的硬性限制,完全无法避免,必然导致次优代码。 - Cody Gray
1
请注意,整个业务都会被编译器内联,包括内联汇编。这是一个示例,其中包含一个练习它的存根函数。同样,唯一可怕的是参数如何传递到块中。如果正确编写,您可以避免所有返回值出块的惩罚。当然,编译器不解析内联汇编指令,因此它不知道足够省略余数的设置,即使“Test”函数从未使用它。但我不确定GCC是否也这样做。 - Cody Gray
1
是的,完全支持且完全可优化。现在,如果你让我提供一些支持这个说法的文档...我得四处搜寻。我脑海中没有可以指出的东西。我的大部分知识来自于编写代码、调整它、分析反汇编,并在具有广泛正确性验证断言的项目中实际使用它。但我认为它曾经发出警告,但现在不再发出警告,这是一个有力的证据表明它正在识别这种习惯用法。很容易看出它如何做到这一点,因为所有的调用约定都是这样工作的。 - Cody Gray
显示剩余7条评论

17

使用哪个取决于您的编译器。这不像C语言那样是标准的。


8
嗯,是的。这些前置下划线的整个意义在于明确表明这是非标准语法。 - Steven Sudit
1
@StevenSudit 不是的,前导下划线意味着“保留”,请参见https://wiki.sei.cmu.edu/confluence/display/c/DCL37-C.+Do+not+declare+or+define+a+reserved+identifier?focusedCommentId=88031790。 - Sapphire_Brick
1
@Sapphire_Brick:特别是“保留供实现使用”,这意味着当您看到它们时,您知道涉及到实现特定的行为。 - Ben Voigt
1
这个回答应该是一条评论,它并不是一个真正的答案。 - Ayberk Özgür
2
@AyberkÖzgür:OP 想知道该使用哪个。答案是他在这件事上没有选择,他必须使用编译器支持的那个。你不能在 Visual C++ 中使用 GCC 内联汇编,也不能在 GCC 中使用 MSVC 内联汇编。 - Ben Voigt
我认为这是一个简洁的答案。 - smwikipedia

15

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__


1
OP询问的是__asm { ... };(有大括号,不是圆括号),因此它绝对不是GNU C内联汇编。Clang和MSVC支持MSVC风格的内联汇编,但gcc不支持。 - Peter Cordes
@PeterCordes 感谢提供信息。部分回答这里是因为涉及到GCC标签,另一部分因为Google搜索理论。现在看历史记录,GCC标签并非由OP添加,但也不是由我添加的;-) - Ciro Santilli OurBigBook.com
@PeterCordes,除了gcc之外,您能否还将适当的Microsoft编译器标签添加到问题中?谢天谢地,我不知道它是哪一个 :-) - Ciro Santilli OurBigBook.com
Visual C++是微软实现的C++方言的标签,由MSVC实现。 - Peter Cordes

8

使用gcc编译器,这并没有太大的区别。 asm__asm__asm__ 都是一样的,它们只是用于避免命名空间冲突的目的(例如,有用户定义的名为asm的函数等)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接