簡體   English   中英

GCC內聯匯編:“g”約束和參數大小

[英]GCC inline assembly: “g” constraint and parameter size

背景

我知道使用內聯匯編解決以下問題是個壞主意。 我目前正在學習內聯匯編作為linux內核類的一部分,這是該類的一個賦值的一部分。

安裝程序

下面的開頭是一段幾乎正確的代碼片段,而不是段錯誤。 它是一個函數,副本的子src開始於索引s_idx和結束(僅僅)在索引e_idx到預分配的dest僅使用內聯組件。

static inline char *asm_sub_str(char *dest, char *src, int s_idx, int e_idx) {
  asm("addq %q2, %%rsi;"  /* Add start index to src (ptrs are 64-bit) */
      "subl %k2, %%ecx;"  /* Get length of substr as e - s (int is 32-bit) */
      "cld;"              /* Clear direction bit (force increment) */
      "rep movsb;"        /* Move %ecx bytes of str at %esi into str at %edi */
      : /* No Ouputs */
      : "S" (src), "D" (dest), "g" (s_idx), "c" (e_idx)
      : "cc", "memory"
      );

  return dest;
}

此代碼的問題是第二個輸入參數的約束。 使用gcc的默認優化和-ggdb編譯時,會生成以下程序集:

Dump of assembler code for function asm_sub_str:
   0x00000000004008e6 <+0>:     push   %rbp
   0x00000000004008e7 <+1>:     mov    %rsp,%rbp
   0x00000000004008ea <+4>:     mov    %rdi,-0x8(%rbp)
   0x00000000004008ee <+8>:     mov    %rsi,-0x10(%rbp)
   0x00000000004008f2 <+12>:    mov    %edx,-0x14(%rbp)
   0x00000000004008f5 <+15>:    mov    %ecx,-0x18(%rbp)
   0x00000000004008f8 <+18>:    mov    -0x10(%rbp),%rax
   0x00000000004008fc <+22>:    mov    -0x8(%rbp),%rdx
   0x0000000000400900 <+26>:    mov    -0x18(%rbp),%ecx
   0x0000000000400903 <+29>:    mov    %rax,%rsi
   0x0000000000400906 <+32>:    mov    %rdx,%rdi
   0x0000000000400909 <+35>:    add    -0x14(%rbp),%rsi
   0x000000000040090d <+39>:    sub    -0x14(%rbp),%ecx
   0x0000000000400910 <+42>:    cld    
   0x0000000000400911 <+43>:    rep movsb %ds:(%rsi),%es:(%rdi)
   0x0000000000400913 <+45>:    mov    -0x8(%rbp),%rax
   0x0000000000400917 <+49>:    pop    %rbp
   0x0000000000400918 <+50>:    retq 

這與第二個輸入參數的約束設置為"m"而不是"g"時生成的程序集相同,這使我相信編譯器正在有效地選擇"m"約束。 在使用gdb執行這些指令時,我發現有問題的指令是+35 ,它將起始偏移索引s_idx%rsisrc指針。 問題當然是s_idx只有32位,而靜態上該位置的64位整數的高4字節不一定是0.在我的機器上,它實際上是非零的並且導致添加混亂%rsi高4字節,導致指令+43

問題

當然,上面的解決方案是將參數2的約束更改為"r"以便將其置於其自己的64位寄存器中,其中前4個字節被正確歸零並將其稱為一天。 相反,我的問題是為什么當表達式"%q2"表示參數2的值將用作64位值時,gcc將"g"約束解析為"m"而不是"r"

我不知道很多有關GCC如何解析內聯匯編,我知道有沒有真的在裝配打字的感覺,但我認為,GCC可以識別的有效隱式轉換s_idxlong時,它被用來作為64第一個內聯指令中的位值。 FWIW,如果我明確地將"g" (s_idx)改為"g" ((long) s_idx) ,則gcc "g" ((long) s_idx) "g"約束解析為"r"因為(long) s_idx是臨時值。 我認為gcc也可以隱含地做到這一點?

但我認為gcc可以在第一個內聯指令中用作64位值時,將s_idx的有效隱式s_idxlong

不,GCC只着眼於約束,而不是asm編譯周圍的代碼時模板串上 填充%模板操作數的gcc部分與周圍代碼的寄存器分配和代碼生成完全分開。

沒有什么可以檢查是否理智或理解正在使用模板操作數的上下文。也許你有一個16位輸入並希望用vmovd %k[input], %%xmm0 / vpbroadcastw %%xmm0, %%ymm0將其復制到向量寄存器vpbroadcastw %%xmm0, %%ymm0 高16位被忽略,因此您不希望gcc浪費時間零或為您進行符號擴展。 但是你絕對想要使用vmovd而不是vpinsrw $0, %[input], %%xmm0 ,因為那會更多vpinsrw $0, %[input], %%xmm0並且具有錯誤的依賴性。 對於所有gcc知道或關心,您可以在asm注釋行中使用操作數,例如"# low word of input = %h2 \\n #low "# low word of input = %h2 \\n

GNU C inline asm的設計使得約束可以告訴編譯器它需要知道的一切。 因此,您需要手動將s_idxlong

您不需要為ECX轉換輸入,因為sub指令將隱式地對結果進行零擴展(進入RCX)。 您的輸入是簽名類型,但可能您希望差異始終是正面的。

必須始終假定寄存器輸入具有超出輸入類型寬度的高垃圾。 這類似於x86-64 System V調用約定中的函數args 可以具有高位32位的垃圾 ,但是(我假設)沒有關於擴展到32位的未寫規則。 (請注意,在函數內聯之后,你的asm語句的輸入可能不是函數args。你不想使用__attribute__((noinline)) ,正如我所說它無論如何也無濟於事。)


讓我相信編譯器正在有效地選擇“m”約束。

是的, gcc -O0在每個C語句之間將所有內容溢出到內存中(因此如果在斷點處停止,則可以使用調試器更改它)。 因此,內存操作數是編譯器最有效的選擇。 它需要一個加載指令才能將其恢復到寄存器中。 即,數值在內存中的前asm聲明,在-O0

(clang在多選項約束下很糟糕,即使在-O3時也會選擇內存,即使這意味着先溢出,但gcc沒有那個問題。)

當輸入是數字文字常量時, gcc -O0 (和clang )將使用立即數作為g約束,例如"g" (1234) 在你的情況下,你得到:

    ...
    addq $1234, %rsi; 
    subl $1234, %ecx; 
    rep movsb
    ...

"g" ((long)s_idx)這樣的輸入即使在-O0也會使用寄存器,就像x+y或任何其他臨時結果一樣(只要s_idxlong )。 有趣的是,偶數(unsigned)導致了一個寄存器操作數,即使intunsigned的大小相同,而且強制轉換沒有指令。 此時你正好看到gcc -O0優化程度,因為你得到的更多依賴於gcc內部設計的方式而不是有意義或有效的方式。


如果要查看有趣的asm,請啟用優化編譯 請參閱如何從GCC /鏗鏘聲組件輸出中刪除“噪音”? ,特別是Matt Godbolt的CppCon2017關於查看編譯器輸出的鏈接。

雖然在沒有優化的情況下檢查asm對於內聯asm來說也是好的; 你可能沒有意識到與使用問題q覆蓋,如果它只是注冊,但它仍然一個問題。 檢查它在-O3如何內聯到幾個不同的調用者也是有用的(特別是如果你使用一些編譯時常量輸入進行測試)。


你的代碼嚴重破壞了

除了上面討論的高垃圾問題,你修改輸入操作數寄存器而不告訴編譯器。

通過使其中一些"+"讀/寫輸出來解決此問題意味着默認情況下您的asm語句不再是volatile ,因此如果輸出未使用,編譯器將對其進行優化。 (這包括在函數內聯之后,因此return dest對於獨立版本是足夠的,但是如果調用者忽略返回值則不在內聯之后。)

你確實使用了"memory" clobber,因此編譯器會假設你讀/寫內存。 你可以告訴它你閱讀和寫作哪個內存,因此它可以在你的副本更有效地優化。 請參閱內聯GNU匯編程序中的獲取字符串長度 :您可以使用虛擬內存輸入/輸出約束,如"m" (*(const char (*)[]) src)

char *asm_sub_str_fancyconstraints(char *dest, char *src, int s_idx, int e_idx) {
  asm (
      "addq %[s_idx], %%rsi; \n\t"  /* Add start index to src (ptrs are 64-bit) */
      "subl %k[s_idx], %%ecx;          \n\t"  /* Get length of substr as e - s (int is 32-bit) */

      // the calling convention requires DF=0, and inline-asm can safely assume it, too
      // (it's widely done, including in the Linux kernel)
      //"cld;"              /* Clear direction bit (force increment) */

      "rep movsb;                \n\t"        /* Move %ecx bytes of str at %esi into str at %edi */
      : [src]"+&S" (src), [dest]"+D" (dest), [e_idx]"+c" (e_idx)
        , "=m" (*(char (*)[]) dest)     // dummy output: all of dest
      : [s_idx]"g" ((long long)s_idx)
        , "m" (*(const char (*)[]) src) // dummy input: tell the compiler we read all of src[0..infinity]
      : "cc"
      );

  return 0; // asm statement not optimized away, even without volatile,
            //  because of the memory output.
            // Just like dest++; could optimize away, but *dest = 0; couldn't.
}

格式化:請注意在每行末尾使用\\n\\t以提高可讀性; 否則asm指令全部在一行上,僅由; (如果你正在檢查你的asm模板是如何工作的,它會很好地組合,但不是很容易閱讀。)

這將編譯(使用gcc -O3)

asm_sub_str_fancyconstraints:
    movslq  %edx, %rdx        # from the (long long)s_idx
    xorl    %eax, %eax        # from the return 0, which I changed to test that it doesn't optimize away
    addq %rdx, %rsi; 
    subl %edx, %ecx;          # your code zero-extends (e_idx - s_idx)
    rep movsb;                

    ret

用gcc + clang在Godbolt編譯器資源管理器上放了這個+其他幾個版本 一個更簡單的版本修復了錯誤,但仍然使用"memory" clobber + asm volatile來獲得正確性,編譯時優化成本高於告訴編譯器讀取和寫入哪個內存的版本。


早期的破壞 :注意"+&S"約束:

如果由於一些奇怪的原因,編譯器知道src地址和s_idx相等,它可以對兩個輸入使用相同的寄存器( esi/rsi )。 這將導致在sub中使用之前修改s_idx 聲明保持src的寄存器早期被破壞(在最后一次讀取所有輸入寄存器之前)將強制編譯器選擇不同的寄存器。

請參閱上面的Godbolt鏈接,了解如果沒有&為早期破壞導致破壞的呼叫者。 (但只有無意義的src = (char*)s_idx; )。 早期刪除聲明通常是多指令asm語句所必需的,以防止更真實的破壞可能性,因此請務必記住這一點,並且只有當您確定任何只讀輸入與共享寄存器時才能將其保留。輸出或輸入/輸出操作數。 (當然使用特定寄存器約束限制了這種可能性。)

我在ecx省略了e_idx中的早期clobber聲明,因為唯一的“free”參數是s_idx ,並且將它們放在同一個寄存器中將導致sub same,samerep movsb根據需要運行0次迭代。


讓編譯器進行數學運算當然會更有效率,並且只需要在正確的寄存器中請求rep movsb的輸入。 特別是如果兩個e_idxs_idx是編譯時間常數,這是愚蠢的,強制編譯器mov立即到寄存器,然后減去另一個立竿見影。

或者甚至更好,不要使用內聯asm。 (但如果你真的想要rep movsb來測試它的性能,那么內聯asm就是一種方法.gcc還有調整選項來控制memcpy聯的方式,如果有的話。)

如果您可以避免,請不要建議你https://gcc.gnu.org/wiki/DontUseInlineAsm ,內聯asm答案是完整的。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM