c++ asm example




'asm','__ asm'和'__asm__'有什麼區別? (3)

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的商業編譯器使用:

  • 你必須告訴編譯器你破壞了什麼。 如果不這樣做,將導致以非顯而易見的難以調試的方式破壞周圍的代碼。
  • 功能強大但難以閱讀,學習和使用語法來告訴編譯器如何提供輸入,以及在何處查找輸出。 例如, "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位整數,或者進行全乘(32x32-> 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
}

更新:顯然在eaxedx: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個輸出。)

據我所知, __asm { ... };之間的唯一區別__asm { ... };__asm__("..."); 是第一個使用mov eax, var ,第二個使用movl %0, %%eax使用:"=r" (var) 。 還有什麼其他差異? 那麼asm呢?


海灣合作委員會中的asm vs __asm__

asm不能與-std=c99 ,你有兩種選擇:

  • 使用__asm__
  • 使用-std=gnu99

更多細節: 錯誤:'asm'未聲明(首次使用此功能)

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__ ,這是記錄在案的。


您使用哪一個取決於您的編譯器。 這不像C語言那樣標準。







inline-assembly