簡體   English   中英

為什么GCC為幾乎相同的C代碼生成如此完全不同的程序集?

[英]Why does GCC generate such radically different assembly for nearly the same C code?

在編寫優化的ftol函數時,我在GCC 4.6.1發現了一些非常奇怪的行為。 讓我先向您展示代碼(為清楚起見,我標記了差異):

fast_trunc_one,C:

int fast_trunc_one(int i) {
    int mantissa, exponent, sign, r;

    mantissa = (i & 0x07fffff) | 0x800000;
    exponent = 150 - ((i >> 23) & 0xff);
    sign = i & 0x80000000;

    if (exponent < 0) {
        r = mantissa << -exponent;                       /* diff */
    } else {
        r = mantissa >> exponent;                        /* diff */
    }

    return (r ^ -sign) + sign;                           /* diff */
}

fast_trunc_two,C:

int fast_trunc_two(int i) {
    int mantissa, exponent, sign, r;

    mantissa = (i & 0x07fffff) | 0x800000;
    exponent = 150 - ((i >> 23) & 0xff);
    sign = i & 0x80000000;

    if (exponent < 0) {
        r = (mantissa << -exponent) ^ -sign;             /* diff */
    } else {
        r = (mantissa >> exponent) ^ -sign;              /* diff */
    }

    return r + sign;                                     /* diff */
}

好像是對的嗎? GCC不同意。 gcc -O3 -S -Wall -o test.s test.c編譯后,這是程序集輸出:

fast_trunc_one,生成:

_fast_trunc_one:
LFB0:
    .cfi_startproc
    movl    4(%esp), %eax
    movl    $150, %ecx
    movl    %eax, %edx
    andl    $8388607, %edx
    sarl    $23, %eax
    orl $8388608, %edx
    andl    $255, %eax
    subl    %eax, %ecx
    movl    %edx, %eax
    sarl    %cl, %eax
    testl   %ecx, %ecx
    js  L5
    rep
    ret
    .p2align 4,,7
L5:
    negl    %ecx
    movl    %edx, %eax
    sall    %cl, %eax
    ret
    .cfi_endproc

fast_trunc_two,生成:

_fast_trunc_two:
LFB1:
    .cfi_startproc
    pushl   %ebx
    .cfi_def_cfa_offset 8
    .cfi_offset 3, -8
    movl    8(%esp), %eax
    movl    $150, %ecx
    movl    %eax, %ebx
    movl    %eax, %edx
    sarl    $23, %ebx
    andl    $8388607, %edx
    andl    $255, %ebx
    orl $8388608, %edx
    andl    $-2147483648, %eax
    subl    %ebx, %ecx
    js  L9
    sarl    %cl, %edx
    movl    %eax, %ecx
    negl    %ecx
    xorl    %ecx, %edx
    addl    %edx, %eax
    popl    %ebx
    .cfi_remember_state
    .cfi_def_cfa_offset 4
    .cfi_restore 3
    ret
    .p2align 4,,7
L9:
    .cfi_restore_state
    negl    %ecx
    sall    %cl, %edx
    movl    %eax, %ecx
    negl    %ecx
    xorl    %ecx, %edx
    addl    %edx, %eax
    popl    %ebx
    .cfi_restore 3
    .cfi_def_cfa_offset 4
    ret
    .cfi_endproc

這是一個極端的差異。 這實際上也出現在配置文件中, fast_trunc_onefast_trunc_two快30% fast_trunc_two 現在我的問題是:是什么導致這個?

已更新以與OP的編輯同步

通過修改代碼,我已經設法看到GCC如何優化第一種情況。

在我們理解為什么它們如此不同之前,首先我們必須了解GCC如何優化fast_trunc_one()

信不信由你, fast_trunc_one()正在優化:

int fast_trunc_one(int i) {
    int mantissa, exponent;

    mantissa = (i & 0x07fffff) | 0x800000;
    exponent = 150 - ((i >> 23) & 0xff);

    if (exponent < 0) {
        return (mantissa << -exponent);             /* diff */
    } else {
        return (mantissa >> exponent);              /* diff */
    }
}

這將生成與原始fast_trunc_one()完全相同的程序集 - 注冊名稱和所有內容。

請注意, fast_trunc_one()的程序fast_trunc_one()沒有xor 這就是為我帶來的東西。


怎么會這樣?


第1步: sign = -sign

首先,我們來看看sign變量。 由於sign = i & 0x80000000; sign只有兩個可能的值:

  • sign = 0
  • sign = 0x80000000

現在認識到,在這兩種情況下, sign == -sign 因此,當我將原始代碼更改為:

int fast_trunc_one(int i) {
    int mantissa, exponent, sign, r;

    mantissa = (i & 0x07fffff) | 0x800000;
    exponent = 150 - ((i >> 23) & 0xff);
    sign = i & 0x80000000;

    if (exponent < 0) {
        r = mantissa << -exponent;
    } else {
        r = mantissa >> exponent;
    }

    return (r ^ sign) + sign;
}

它生成與原始fast_trunc_one()完全相同的程序集。 我會為你免費組裝,但它是相同的 - 注冊名稱和所有。


步驟2:數學簡化: x + (y ^ x) = y

sign只能取兩個值中的一個, 00x80000000

  • x = 0 ,那么x + (y ^ x) = y然后是平凡的。
  • 通過0x80000000添加和xoring是相同的。 它翻轉了標志位。 因此,當x = 0x80000000時, x + (y ^ x) = y也成立。

因此, x + (y ^ x)減少到y 代碼簡化為:

int fast_trunc_one(int i) {
    int mantissa, exponent, sign, r;

    mantissa = (i & 0x07fffff) | 0x800000;
    exponent = 150 - ((i >> 23) & 0xff);
    sign = i & 0x80000000;

    if (exponent < 0) {
        r = (mantissa << -exponent);
    } else {
        r = (mantissa >> exponent);
    }

    return r;
}

再次,這編譯成完全相同的程序集 - 寄存器名稱和所有。


以上版本最終簡化為:

int fast_trunc_one(int i) {
    int mantissa, exponent;

    mantissa = (i & 0x07fffff) | 0x800000;
    exponent = 150 - ((i >> 23) & 0xff);

    if (exponent < 0) {
        return (mantissa << -exponent);             /* diff */
    } else {
        return (mantissa >> exponent);              /* diff */
    }
}

這幾乎就是GCC在裝配中生成的內容。


那么為什么編譯器不能將fast_trunc_two()優化為同樣的東西呢?

fast_trunc_one()的關鍵部分是x + (y ^ x) = y優化。 fast_trunc_two()x + (y ^ x)表達式在分支上被拆分。

我懷疑這可能足以讓GCC混淆不進行優化。 (它需要將^ -sign提升出分支並將其合並到末尾的r + sign中。)

例如,這會生成與fast_trunc_one()相同的程序集:

int fast_trunc_two(int i) {
    int mantissa, exponent, sign, r;

    mantissa = (i & 0x07fffff) | 0x800000;
    exponent = 150 - ((i >> 23) & 0xff);
    sign = i & 0x80000000;

    if (exponent < 0) {
        r = ((mantissa << -exponent) ^ -sign) + sign;             /* diff */
    } else {
        r = ((mantissa >> exponent) ^ -sign) + sign;              /* diff */
    }

    return r;                                     /* diff */
}

這是編譯器的本質。 假設他們將采取最快或最好的路徑,是非常錯誤的。 任何暗示你不需要對你的代碼做任何事情來優化的人,因為“現代編譯器”填補空白,做最好的工作,制作最快的代碼等。實際上我看到gcc從3.x變得更糟。 x至少在手臂上。 到目前為止,4.x可能已達到3.x,但在早期它會產生較慢的代碼。 通過練習,您可以學習如何編寫代碼,這樣編譯器就不必努力工作,從而產生更一致和預期的結果。

這里的錯誤是你對將要產生的東西的期望,而不是實際產生的東西。 如果希望編譯器生成相同的輸出,請為其輸入相同的輸入。 不是數學上相同,不是有點相同,但實際上是相同的,沒有不同的路徑,沒有從一個版本到另一個版本的共享或分發操作。 這是一個很好的練習,可以理解如何編寫代碼並查看編譯器使用它做什么。 不要錯誤地假設因為一天的一個處理器目標的一個版本的gcc產生了一定的結果,這是所有編譯器和所有代碼的規則。 您必須使用許多編譯器和許多目標來了解正在發生的事情。

gcc非常討厭,我邀請你看看幕后,看看gcc的膽量,嘗試添加一個目標或自己修改一些東西。 它幾乎沒有用膠帶和撈絲固定在一起。 在關鍵位置添加或刪除了一行額外的代碼,它崩潰了。 事實上,它已經產生了可用的代碼,這是令人高興的事情,而不是擔心為什么它沒有滿足其他期望。

你看過gcc不同版本的產品嗎? 3.x和4.x特別是4.5 vs 4.6 vs 4.7等? 對於不同的目標處理器,x86,arm,mips等或x86的不同風格,如果這是你使用的本機編譯器,32位vs 64位等? 然后llvm(clang)針對不同的目標?

神秘在完成分析/優化代碼問題所需的思考過程中做得非常出色,期望編譯器能夠提出任何一個,而不是任何“現代編譯器”。

沒有進入數學屬性,這種形式的代碼

if (exponent < 0) {
  r = mantissa << -exponent;                       /* diff */
} else {
  r = mantissa >> exponent;                        /* diff */
}
return (r ^ -sign) + sign;                           /* diff */

將引導編譯器到A:以該形式實現它,執行if-then-else然后收斂於公共代碼以完成並返回。 或者B:保存分支,因為這是函數的尾部。 也不用擔心使用或保存r。

if (exponent < 0) {
  return((mantissa << -exponent)^-sign)+sign;
} else {
  return((mantissa << -exponent)^-sign)+sign;
}

然后你可以進入,因為Mystical指出sign符號變量一起消失所有代碼所寫的。 我不希望編譯器看到符號變量消失所以你應該自己做,而不是強迫編譯器試圖找出它。

這是深入了解gcc源代碼的絕佳機會。 看來你已經找到了一個案例,優化器在一個案例中看到了一件事,在另一個案例中看到了另一件事。 然后采取下一步,看看你是否不能讓gcc看到這種情況。 每個優化都在那里,因為一些個人或團體認可了優化並故意將其放在那里。 每次有人必須將它放在那里(然后測試它,然后將其保存到將來),這種優化就可以在那里工作。

絕對不要假設代碼越少越快,代碼越慢,就越容易創建並找到不正確的示例。 通常情況下,更少的代碼比更多的代碼更快。 正如我從一開始就證明的那樣,你可以創建更多的代碼來保存分支或者循環等,並且最終的結果是更快的代碼。

底線是您為編譯器提供了不同的源,並期望得到相同的結果。 問題不在於編譯器輸出,而在於用戶的期望。 對於特定的編譯器和處理器來說,相當容易演示,添加一行代碼會使整個函數顯着變慢。 例如,為什么改變a = b + 2; 到a = b + c + 2; 原因_fill_in_the_blank_compiler_name_生成完全不同且速度較慢的代碼? 答案當然是編譯器在輸入上輸入了不同的代碼,因此編譯器生成不同的輸出是完全有效的。 (更好的是當您交換兩條不相關的代碼行並導致輸出發生顯着變化時)輸入的復雜性和大小與輸出的復雜性和大小之間沒有預期的關系。 把這樣的東西喂成clang:

for(ra=0;ra<20;ra++) dummy(ra);

它產生了60-100行匯編程序。 它展開了循環。 我沒有計算行,如果你考慮它,它必須添加,將結果復制到函數調用的輸入,進行函數調用,最少三個操作。 所以取決於至少60個指令的目標,如果每個循環四個,則為80,如果每個循環為5,則為100,等等。

Mysticial已經給出了一個很好的解釋,但是我想我會補充說,為什么編譯器會為一個編譯器而不是另一個編譯器進行優化,實際上沒有什么根本的。

例如,LLVM的clang編譯器為兩個函數提供相同的代碼(函數名除外),給出:

_fast_trunc_two:                        ## @fast_trunc_one
        movl    %edi, %edx
        andl    $-2147483648, %edx      ## imm = 0xFFFFFFFF80000000
        movl    %edi, %esi
        andl    $8388607, %esi          ## imm = 0x7FFFFF
        orl     $8388608, %esi          ## imm = 0x800000
        shrl    $23, %edi
        movzbl  %dil, %eax
        movl    $150, %ecx
        subl    %eax, %ecx
        js      LBB0_1
        shrl    %cl, %esi
        jmp     LBB0_3
LBB0_1:                                 ## %if.then
        negl    %ecx
        shll    %cl, %esi
LBB0_3:                                 ## %if.end
        movl    %edx, %eax
        negl    %eax
        xorl    %esi, %eax
        addl    %edx, %eax
        ret

此代碼不像OP中的第一個gcc版本那么短,但不像第二個那樣長。

來自另一個編譯器(我不會命名)的代碼,為x86_64編譯,為這兩個函數生成:

fast_trunc_one:
        movl      %edi, %ecx        
        shrl      $23, %ecx         
        movl      %edi, %eax        
        movzbl    %cl, %edx         
        andl      $8388607, %eax    
        negl      %edx              
        orl       $8388608, %eax    
        addl      $150, %edx        
        movl      %eax, %esi        
        movl      %edx, %ecx        
        andl      $-2147483648, %edi
        negl      %ecx              
        movl      %edi, %r8d        
        shll      %cl, %esi         
        negl      %r8d              
        movl      %edx, %ecx        
        shrl      %cl, %eax         
        testl     %edx, %edx        
        cmovl     %esi, %eax        
        xorl      %r8d, %eax        
        addl      %edi, %eax        
        ret                         

這很有趣,因為它計算if兩邊然后在最后使用條件移動來選擇正確的一個。

Open64編譯器生成以下內容:

fast_trunc_one: 
    movl %edi,%r9d                  
    sarl $23,%r9d                   
    movzbl %r9b,%r9d                
    addl $-150,%r9d                 
    movl %edi,%eax                  
    movl %r9d,%r8d                  
    andl $8388607,%eax              
    negl %r8d                       
    orl $8388608,%eax               
    testl %r8d,%r8d                 
    jl .LBB2_fast_trunc_one         
    movl %r8d,%ecx                  
    movl %eax,%edx                  
    sarl %cl,%edx                   
.Lt_0_1538:
    andl $-2147483648,%edi          
    movl %edi,%eax                  
    negl %eax                       
    xorl %edx,%eax                  
    addl %edi,%eax                  
    ret                             
    .p2align 5,,31
.LBB2_fast_trunc_one:
    movl %r9d,%ecx                  
    movl %eax,%edx                  
    shll %cl,%edx                   
    jmp .Lt_0_1538                  

fast_trunc_two相似但不相同的代碼。

無論如何,當涉及到優化時,它是一個樂透 - 它就是它......它並不總是很容易知道為什么你的代碼以任何特定方式編譯。

暫無
暫無

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

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