'asm','__ asm'和'__asm__'有什么区别?

Adr*_*ian 33 c assembly gcc inline-assembly visual-c++

据我所知道的,唯一的区别__asm { ... };,并__asm__("...");是第一个使用mov eax, var第二个使用movl %0, %%eax:"=r" (var)结尾.还有什么其他差异?那又怎么样asm

Pet*_*des 33

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有关.在标签wiki有很多好东西在一般ASM,只链接,对GNU内联汇编.(该答案中的内容也适用于非x86平台上的GNU内联asm.)

GNU C inline asm语法由gcc,clang,icc和一些实现GNU C的商业编译器使用:

  • 你必须告诉编译器你破坏了什么.如果不这样做,将导致以非显而易见的难以调试的方式破坏周围的代码.
  • 功能强大但难以阅读,学习和使用语法来告诉编译器如何提供输入以及在何处查找输出.例如,在内联asm运行之前,"c" (shift_count)将让编译器将shift_count变量放入其中ecx.
  • 因为asm必须在一个字符串常量内,所以对于大块代码来说是额外的笨重.所以你通常需要

    "insn   %[inputvar], %%reg\n\t"       // comment
    "insn2  %%reg, %[outputvar]\n\t"
    
    Run Code Online (Sandbox Code Playgroud)
  • 非常无情/更难,但允许更低的开销esp.用于包装单个指令.(包装单个指令是原始的设计意图,这就是为什么你必须特别告诉编译器有关早期的clobbers,以阻止它使用相同的寄存器输入和输出,如果这是一个问题.)


示例:全宽整数除法(div)

在32位CPU上,将64位整数除以32位整数,或者进行全乘(32x32-> 64),可以从内联asm中受益.gcc和clang没有利用idivfor (int64_t)a / (int32_t)b,可能是因为如果结果不适合32位寄存器,指令就会出错.所以不像这个Q&A关于从一个获得商和余数div,这是内联asm的用例.(除非有办法通知编译器结果是否合适,所以idiv不会出错.)

我们将使用调用约定将一些args放在寄存器中(hi即使在正确的寄存器中),以显示一个更接近你在内联这样一个小函数时看到的情况.


MSVC

使用inline-asm时要小心register-arg调用约定.显然,内联asm支持的设计/实现非常糟糕,以至于编译器可能无法在内联asm周围保存/恢复arg寄存器,如果这些arg没有在内联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
}
Run Code Online (Sandbox Code Playgroud)

更新:在离开显然的值eaxedx:eax,然后脱落的非空隙函数的结束(无return)被支持,内联,即使.我认为只有在asm语句后面没有代码时才有效.这避免了输出的存储/重新加载(至少对于输出quotient),但我们无法对输入做任何事情.在具有堆栈args的非内联函数中,它们已经在内存中,但在这个用例中,我们正在编写一个可以有用内联的小函数.


在rextester /O2 用MSVC 19.00.23026编译(用一个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
Run Code Online (Sandbox Code Playgroud)

有大量额外的mov指令,编译器甚至没有接近优化任何它.我想也许它会看到并理解mov tmp, edx内联asm 的内部,并将其作为商店premainder.但是,我想这需要premainder在内联asm块之前从堆栈加载到寄存器中.

这个功能实际上是雪上加霜_vectorcall比正常的一切-上的堆栈ABI.寄存器中有两个输入,它将它们存储到内存中,因此内联asm可以从命名变量加载它们.如果这是内联的,那么更多的参数可能会出现在regs中,而且必须将它们全部存储起来,所以asm会有内存操作数!因此,与gcc不同,我们从内联中获得的收益并不高.

*premainder = tmp在asm块内指写在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个params进入regs(并且被调用者不会从堆栈中弹出args,这就是ret 8MSVC输出中的内容.)

// 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;
}
Run Code Online (Sandbox Code Playgroud)

gcc -m64 -O3 -mabi=ms -fverbose-asm.编译.使用-m32,你可以获得3个负载,idiv和商店,正如你可以从更改神线链接中的内容中看到的那样.

mov     eax, ecx  # lo, lo
idivl  r9d      # divisor
mov     DWORD PTR [r8], edx       # *premainder_7(D), rem
ret
Run Code Online (Sandbox Code Playgroud)

对于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
Run Code Online (Sandbox Code Playgroud)

MSVC使用13条指令(不包括ret),与gcc的4相比.内联,正如我所说,它可能只编译为一条,而MSVC仍然可能使用9条.(它不需要保留堆栈空间或负载premainder;我假设它仍然需要存储3个输入中的大约2个.然后它在asm中重新加载它们,运行idiv,存储两个输出,并将它们重新加载到asm之外.因此,4个加载/存储用于输入,另外4个输出.)

  • 对于它的价值,[这是我如何在 MSVC 的内联汇编中编写该函数](http://pastebin.com/YJaN3zeH)。底部展示了拆解图。我使用“__stdcall”编译了该函数,但“__cdecl”也同样有效。不要使用 `__vectorcall` 或 `__fastcall`,它们只会悲观内联汇编。请注意,输出与 GCC 的输出几乎相同,除了需要显式地将参数从堆栈加载到寄存器中之外。这是 MSVC 内联汇编的硬限制,完全不可避免,并且不可避免地会导致次优代码。 (2认同)

Ben*_*igt 16

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

  • 嗯,是的 这些主要下划线的重点是要明确这是不标准的. (4认同)
  • 第一个在VC++中工作,第二个在gcc中工作 (4认同)
  • @AyberkÖzgür:OP 想知道使用哪一个。答案是他在这件事上没有选择,他必须使用他的编译器支持的。您不能在 Visual C++ 中使用 GCC 内联 asm,也不能在 gcc 中使用 MSVC 内联 asm。 (2认同)

Cir*_*四事件 9

asm__asm__海湾合作委员会

asm不适用于-std=c99,您有两种选择:

  • __asm__
  • -std=gnu99

更多详细信息:错误:'asm' 未声明(首次在此函数中使用)

__asm__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 },
Run Code Online (Sandbox Code Playgroud)

所以我只会使用__asm__记录在案的。


小智 5

使用gcc编译器,它没有太大的区别.asm或者__asm或者__asm__是相同的,他们只是用它来避免冲突的命名空间的目的(有用户定义函数名ASM等)