GCC neden neredeyse aynı C kodu için bu kadar radikal olarak farklı bir montaj üretiyor?


184

Optimize edilmiş bir ftolişlev yazarken bazı garip davranışlar buldum GCC 4.6.1. Önce kodu göstereyim (açıklık için farklılıkları işaretledim):

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 */
}

Aynı görünüyor mu? GCC buna katılmıyor. Bununla derledikten sonra gcc -O3 -S -Wall -o test.s test.cmontaj çıktısı:

fast_trunc_one, oluşturuldu:

_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, oluşturuldu:

_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

Bu aşırı bir fark. Bu aslında profilde de görülür, fast_trunc_oneyaklaşık% 30 daha hızlıdır fast_trunc_two. Şimdi sorum: buna ne sebep oluyor?


1
Test amaçlı olarak bir özünü oluşturdu burada sen / kopyalama kaynağı yapıştırın ve diğer sistemlere / GCC sürümlerinde hata yeniden olmadığını görmek kolaylıkla nerede.
orlp

12
Test senaryolarını kendi dizinlerine koyun. Onları derleyin -S -O3 -da -fdump-tree-all. Bu, ara gösterimin birçok anlık görüntüsünü oluşturur. Yan yana (numaralandırılmış) gezinin ve ilk durumda eksik optimizasyonu bulabilmeniz gerekir.
zwol

1
İkinci öneri: Tümünü intdeğiştirin unsigned intve farkın kaybolup kaybolmadığına bakın.
zwol

5
İki fonksiyon biraz farklı matematik yapıyor gibi görünüyor. Sonuçlar aynı olsa da, ifade (r + shifted) ^ signile aynı değildir r + (shifted ^ sign). Sanırım bu optimize ediciyi kafa karıştırıyor mu? FWIW, MSVC 2010 (16.00.40219.01) birbiriyle neredeyse aynı olan listeleri üretir: gist.github.com/2430454
DCoder

1
@DCoder: Kahretsin! Bunu farketmedim. Bu farkın açıklaması değil. Soruyu, bu durumun reddedildiği yeni bir sürümle güncelleyeyim.
orlp

Yanıtlar:


256

OP'nin düzenlemesiyle senkronize edilecek şekilde güncellendi

Kodla uğraşarak, GCC'nin ilk durumu nasıl optimize ettiğini görmeyi başardım.

Neden bu kadar farklı olduklarını anlayabilmemiz için önce GCC'nin nasıl optimizasyon yaptığını anlamalıyız fast_trunc_one().

İster inanın ister inanmayın, fast_trunc_one()bunun için optimize ediliyor:

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 */
    }
}

Bu, orijinal fast_trunc_one()kayıt adları ve her şeyle aynı derlemeyi üretir .

xorMontajda hiçbir s olmadığına dikkat edin fast_trunc_one(). Bunu benim için verdi.


Nasıl yani?


Aşama 1: sign = -sign

İlk olarak, signdeğişkene bir göz atalım . Çünkü alabilecek sign = i & 0x80000000;sadece iki olası değer vardır sign:

  • sign = 0
  • sign = 0x80000000

Şimdi her iki durumda da bunu kabul edin sign == -sign. Bu nedenle, özgün kodu bunu değiştirdiğimde:

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;
}

Orijinal ile tam olarak aynı montajı üretir fast_trunc_one(). Derlemeyi yedekleyeceğim, ama aynı - kayıt isimleri ve hepsi.


Adım 2: Matematiksel indirgeme:x + (y ^ x) = y

signyalnızca iki değerden birini alabilir 0veya 0x80000000.

  • Ne zaman x = 0, o x + (y ^ x) = yzaman önemsiz tutar.
  • Eklemek ve xoring 0x80000000aynıdır. İşaret bitini döndürür. Bu nedenle x + (y ^ x) = yde ne zaman tutar x = 0x80000000.

Bu nedenle, x + (y ^ x)azaltır y. Ve kod bunu basitleştirir:

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;
}

Yine, bu aynı derleme - kayıt adları ve hepsi için derlenir.


Bu yukarıdaki sürüm nihayetinde bunu azaltı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'nin montajda ürettiği tam olarak budur.


Peki derleyici neden fast_trunc_two()aynı şeyi optimize etmiyor ?

Anahtar bölümü içinde fast_trunc_one()olduğu x + (y ^ x) = yoptimizasyonu. Gelen ifade şube genelinde bölünmüş ediliyor.fast_trunc_two()x + (y ^ x)

Bunun, bu optimizasyonu yapmamak için GCC'yi karıştırmak için yeterli olabileceğinden şüpheleniyorum. ( ^ -signDalın dışına çekilip r + signsonunda birleştirilmelidir .)

Örneğin, bu aynı montajı üretir 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 */
}

4
Düzenleme, ikinci düzeltmeye cevap verdim. Mevcut revizyon iki örneği çevirdi ve kodu biraz değiştirdi ... bu kafa karıştırıcı.
Gizemli

2
@nightcracker Endişelenme. Geçerli sürümle senkronize etmek için cevabımı güncelledim.
Gizemli

1
@Mysticial: son ifadeniz artık yeni sürümle doğru değil, cevabınızı geçersiz kılıyor (en önemli soruyu "GCC neden bu kadar radikal olarak farklı bir montaj oluşturuyor" diye cevaplamıyor .)
orlp

11
Yanıt tekrar güncellendi. Yeterince tatmin edici olup olmadığından emin değilim. Ancak, ilgili GCC optimizasyonunun nasıl geçtiğini tam olarak bilmeden çok daha iyi yapabileceğimi sanmıyorum.
Gizemli

4
@Mysticial: Kesinlikle konuşursak, bu kodda imzalı tip yanlış kullanıldığı sürece, derleyicinin burada yaptığı dönüşümlerin neredeyse tamamı, davranışın tanımlanmadığı durumlarda ...
R .. GitHub STOP HELCE ICE

63

Derleyicilerin doğası budur. En hızlı veya en iyi yolu seçeceklerini varsayarsak, oldukça yanlıştır. Eğer "modern derleyiciler" boş doldurun, en iyi işi yapmak, en hızlı kodu, vb çünkü doldurmak için kodunuzu için bir şey yapmak gerekmez ima herkes Aslında ben gcc 3.x daha kötü olsun gördüm En az kolda 4.x. 4.x bu noktaya kadar 3.x'e kadar yakalamış olabilir, ancak daha önce daha yavaş kod üretti. Uygulama ile kodunuzu nasıl yazacağınızı öğrenebilirsiniz, böylece derleyici çok çalışmak zorunda kalmaz ve sonuç olarak daha tutarlı ve beklenen sonuçlar üretir.

Buradaki hata, aslında ne üretildiğine değil, ne üretileceğine dair beklentilerinizdir. Derleyicinin aynı çıktıyı üretmesini istiyorsanız, aynı girdiyi besleyin. Matematiksel olarak aynı değil, aynı değil, ama aslında aynı, farklı yollar yok, bir sürümden diğerine paylaşım veya dağıtım işlemleri yok. Bu, kodunuzu nasıl yazacağınızı ve derleyicilerin onunla ne yaptığını görmede iyi bir alıştırmadır. Bir gün için bir işlemci hedefi için gcc'nin bir sürümünün, tüm derleyiciler ve tüm kodlar için bir kural olan belirli bir sonuç ürettiğini varsaymakla hata yapmayın. Neler olup bittiğini hissetmek için birçok derleyici ve birçok hedef kullanmalısınız.

gcc oldukça kötü, sizi perdenin arkasına bakmaya, gcc'nin bağırsaklarına bakmaya, bir hedef eklemeye ya da bir şeyi kendiniz değiştirmeye davet ediyorum. Koli bandı ve balya teli ile zar zor birlikte tutulur. Kritik yerlere ekstra bir kod satırı eklenir veya kaldırılır ve parçalanır. Kullanılabilir kod üretmiş olması, neden diğer beklentileri karşılamadığı konusunda endişelenmek yerine, memnun edilecek bir şey.

gcc'nin hangi farklı versiyonlarını ürettiğine baktınız mı? 3.x ve 4.x özellikle 4.5 vs 4.6 vs 4.7, vb? ve farklı hedef işlemciler için, x86, kol, mips, vb. ya da kullandığınız yerel derleyici ise, 32 bit vs 64 bit, vb. Ve sonra farklı hedefler için llvm (clang)?

Mystical, kodun analiz edilmesi / optimize edilmesi, bir derleyicinin bunlardan herhangi biriyle gelmesini beklemek için gerekli olan düşünce sürecinde mükemmel bir iş çıkardı.

Matematik özelliklerine girmeden bu formun kodu

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

derleyiciyi A'ya yönlendirecektir: bu formda uygulayın, if-then-else işlemini gerçekleştirin ve ardından bitirmek ve geri dönmek için ortak kod üzerinde birleşin. veya B: bir dalın kaydedilmesi, çünkü bu fonksiyonun kuyruk ucu. Ayrıca r kullanma veya kaydetme ile uğraşmayın.

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

Daha sonra Mystical'in işaret değişkeni yazılı olarak kod için hep birlikte kaybolduğunu belirtti. Derleyicinin işaret değişkeninin gitmesini beklemesini beklemezdim, bu yüzden bunu kendiniz yapmalı ve derleyiciyi anlamaya çalışmak zorunda bırakmamalısınız.

Bu, gcc kaynak kodunu incelemek için mükemmel bir fırsattır. Görünüşe göre, optimize edicinin bir durumda bir şey daha sonra başka bir durumda başka bir şey gördüğü. Sonra bir sonraki adımı atın ve bu durumu görmek için gcc alamıyor musunuz? Her optimizasyon oradadır, çünkü bazı bireyler veya gruplar optimizasyonu tanımış ve kasten oraya koymuştur. Bu optimizasyonun orada olması ve birisinin oraya her koyması gerektiğinde çalışması için (ve sonra test edin ve sonra geleceğe devam edin).

Kesinlikle daha az kodun daha hızlı ve daha fazla kodun daha yavaş olduğunu varsaymayın, bunun doğru olmadığına dair örnekler oluşturmak ve bulmak çok kolaydır. Daha az kodun daha fazla koddan daha hızlı olması daha sık olmayabilir. Ben bu durumda veya döngü, vb dallanma kaydetmek için daha fazla kod oluşturabilir ve net sonuç daha hızlı kod olması rağmen başından itibaren gösterdi.

Sonuç olarak, bir derleyiciye farklı bir kaynak beslediniz ve aynı sonuçları beklediniz. Sorun derleyici çıktısı değil kullanıcının beklentisidir. Belirli bir derleyici ve işlemci için, tüm işlevi önemli ölçüde yavaşlatan bir kod satırı eklenmesi oldukça kolaydır. Örneğin neden a = b + 2; a = b + c + 2'ye; _fill_in_the_blank_compiler_name_ kökten farklı ve daha yavaş bir kod üretmesine neden oluyor? Derleyici olmanın yanıtı girdiye farklı bir kodla beslendiğinden, derleyicinin farklı çıktılar üretmesi mükemmel bir şekilde geçerlidir. (daha da iyi olan, iki ilişkisiz kod satırını değiştirip çıktının önemli ölçüde değişmesine neden olmanızdır) Girdinin karmaşıklığı ve boyutu ile çıktının karmaşıklığı ve boyutu arasında beklenen bir ilişki yoktur.

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

60-100 sıra montajcı arasında bir yerde üretildi. Döngüyü çözdü. Satırları saymadım, eğer düşünürseniz, eklemeniz, sonucu fonksiyon çağrısına girdiye kopyalamanız, fonksiyon çağrısını yapmanız, minimum üç işlem yapmanız gerekir. yani muhtemelen en az 60 komut olan hedefe bağlı olarak, döngü başına dört ise 80, döngü başına beşse 100 vb.


Cevabınızı neden tahrip ettiniz? Oded, düzenleme ile de aynı fikirde değildi ;-).
Peter - Monica'yı

@ PeterA.Schneider'in tüm cevapları aynı tarihte tahrip edilmiş gibi görünüyor. Bence (çalınmış?) Hesap verilerine sahip biri bunu yaptı.
trinity420

23

Mysticial zaten büyük bir açıklama yaptı, ancak FWIW'a bir derleyicinin diğeri için neden optimizasyon yapacağı konusunda temel bir şey olmadığını ekleyeceğimi düşündüm.

clangÖrneğin, LLVM'nin derleyicisi, her iki işlev için de (işlev adı hariç) aynı kodu verir ve şunları verir:

_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

Bu kod OP'nin ilk gcc sürümü kadar kısa değil, ikincisi kadar uzun değil.

X86_64 için derleyen başka bir derleyiciden gelen kod (ad vermeyeceğim) her iki işlev için de bunu üretir:

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                         

ki her iki tarafını da hesaplar ifve sonra doğru olanı seçmek için sonunda koşullu bir hareket kullanır.

Open64 derleyicisi aşağıdakileri üretir:

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                  

ve benzer, ancak aynı değil kodu fast_trunc_two.

Her neyse, optimizasyon söz konusu olduğunda, bu bir piyango - ne olduğu ... Kodunuzun neden belirli bir şekilde derlendiğini bilmek her zaman kolay değildir.


10
Derleyici çok gizli bir süper derleyiciyi adlandırmayacak mı?
orlp

4
Çok Gizli derleyici muhtemelen Intel'dir icc. Ben sadece 32-bit varyantı var ama buna çok benzer kod üretir.
Janus Troelsen

5
Ayrıca ICC olduğuna inanıyorum. Derleyici, işlemcinin komut düzeyinde paralelliğe sahip olduğunu bilir ve böylece her iki dal da aynı anda hesaplanabilir. Koşullu hareketin yükü, yanlış dal tahmininin yükünden çok daha düşüktür.
Filip Navara
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.