Başka ifadelerde GCC'nin __builtin_expect'in avantajı nedir?


144

Kullandıkları bir #definealana rastladım __builtin_expect.

Belgeler diyor ki:

Dahili Fonksiyon: long __builtin_expect (long exp, long c)

__builtin_expectDerleyiciye dal tahmin bilgileri sağlamak için kullanabilirsiniz . Genel olarak, -fprofile-arcsprogramcılar programlarının gerçekte nasıl çalıştığını tahmin etmede kötü bir şekilde kötü olduğundan, bunun için gerçek profil geri bildirimini ( ) kullanmayı tercih etmelisiniz . Ancak, bu verilerin toplanmasının zor olduğu uygulamalar vardır.

Dönüş değeri, expintegral ifadesi olması gereken değerdir . Yerleşik anlambilim, beklenen bir şeydir exp == c. Örneğin:

      if (__builtin_expect (x, 0))
        foo ();

sıfır olmayı umduğumuz için aramayı foobeklemediğimizi xgösterir.

Öyleyse neden doğrudan kullanmıyorsunuz:

if (x)
    foo ();

yerine karmaşık sözdizimi ile __builtin_expect?



3
Sanırım doğrudan kodunuz if ( x == 0) {} else foo();.. ya da sadece if ( x != 0 ) foo();GCC belgelerinden gelen koda eşdeğer olmalıdır.
Nawaz

Yanıtlar:


187

Şunlardan oluşturulacak montaj kodunu düşünün:

if (__builtin_expect(x, 0)) {
    foo();
    ...
} else {
    bar();
    ...
}

Sanırım böyle bir şey olmalı:

  cmp   $x, 0
  jne   _foo
_bar:
  call  bar
  ...
  jmp   after_if
_foo:
  call  foo
  ...
after_if:

Talimatların, bardavanın davadan önce foo(C kodunun aksine) sıralandığını görebilirsiniz . Bu, CPU boru hattını daha iyi kullanabilir, çünkü bir atlama zaten getirilmiş olan talimatları atlar.

Atlama yapılmadan önce altındaki talimatlar ( barkasa) boru hattına itilir. Yana foovaka olası değildir, atlama çok nedenle boru hattı olası değildir dayak, olası değildir.


1
Gerçekten böyle mi çalışıyor? Foo tanımı neden birinci olamaz? Bir prototipiniz olduğu sürece işlev tanımlarının sırası önemsizdir, değil mi?
kingsmasher1

63
Bu işlev tanımlarıyla ilgili değildir. Makine kodunu, CPU'nun yürütülmeyecek talimatları getirme olasılığını azaltacak şekilde yeniden düzenlemekle ilgilidir.
Blagovest Buyukliev

4
Anlıyorum. Yani, yüksek bir olasılık olduğu için x = 0, önce çubuğa verilir. Ve foo, daha sonra tanımlanır çünkü şansı (daha çok kullanım olasılığı) daha azdır, değil mi?
kingsmasher1

1
Ahhh..thanks. Bu en iyi açıklama. Montaj kodu gerçekten hile yaptı :)
kingsmasher1

5
Bu aynı zamanda CPU şube öngörücüsü için ipuçları da
ekleyerek boru hattını

50

GCC 4.8'in onunla ne yaptığını görelim

Blagovest, boru hattını iyileştirmek için şube inversiyonundan bahsetti, ancak mevcut derleyiciler bunu gerçekten yapıyor mu? Hadi bulalım!

olmadan __builtin_expect

#include "stdio.h"
#include "time.h"

int main() {
    /* Use time to prevent it from being optimized away. */
    int i = !time(NULL);
    if (i)
        puts("a");
    return 0;
}

GCC 4.8.2 x86_64 Linux ile derleyin ve kaynak kodunu çözün:

gcc -c -O3 -std=gnu11 main.c
objdump -dr main.o

Çıktı:

0000000000000000 <main>:
   0:       48 83 ec 08             sub    $0x8,%rsp
   4:       31 ff                   xor    %edi,%edi
   6:       e8 00 00 00 00          callq  b <main+0xb>
                    7: R_X86_64_PC32        time-0x4
   b:       48 85 c0                test   %rax,%rax
   e:       75 0a                   jne    1a <main+0x1a>
  10:       bf 00 00 00 00          mov    $0x0,%edi
                    11: R_X86_64_32 .rodata.str1.1
  15:       e8 00 00 00 00          callq  1a <main+0x1a>
                    16: R_X86_64_PC32       puts-0x4
  1a:       31 c0                   xor    %eax,%eax
  1c:       48 83 c4 08             add    $0x8,%rsp
  20:       c3                      retq

Bellekteki talimat sırası değişmedi: önce putsve sonra retqgeri dönün.

İle __builtin_expect

Şimdi aşağıdakilerle değiştirin if (i):

if (__builtin_expect(i, 0))

ve elde ederiz:

0000000000000000 <main>:
   0:       48 83 ec 08             sub    $0x8,%rsp
   4:       31 ff                   xor    %edi,%edi
   6:       e8 00 00 00 00          callq  b <main+0xb>
                    7: R_X86_64_PC32        time-0x4
   b:       48 85 c0                test   %rax,%rax
   e:       74 07                   je     17 <main+0x17>
  10:       31 c0                   xor    %eax,%eax
  12:       48 83 c4 08             add    $0x8,%rsp
  16:       c3                      retq
  17:       bf 00 00 00 00          mov    $0x0,%edi
                    18: R_X86_64_32 .rodata.str1.1
  1c:       e8 00 00 00 00          callq  21 <main+0x21>
                    1d: R_X86_64_PC32       puts-0x4
  21:       eb ed                   jmp    10 <main+0x10>

putsFonksiyonun, en sonuna taşındı retqdönüş!

Yeni kod temel olarak aynıdır:

int i = !time(NULL);
if (i)
    goto puts;
ret:
return 0;
puts:
puts("a");
goto ret;

Bu optimizasyon ile yapılmadı -O0.

Ancak __builtin_expect, CPU'ların onsuzdan daha hızlı çalışan bir örnek yazma konusunda iyi şanslar , CPU'lar o günlerde gerçekten akıllı . Benim saf denemelerim burada .

C ++ 20 [[likely]]ve[[unlikely]]

C ++ 20, bu C ++ yerleşiklerini standardize etti: C ++ 20'nin if-else deyiminde olası / olası olmayan özniteliğini nasıl kullanacaklar ?


1
Pratik bir optimizasyon için __builtin_expect kullanan libdispatch'in dispatch_once işlevine göz atın. Yavaş yol, bir defaya mahsus çalışır ve şube tahmincisine hızlı yolun alınması gerektiğini ima etmek için __builtin_expect'ten yararlanır. Hızlı yol hiç kilit kullanmadan çalışır! mikeash.com/pyblog/…
Adam Kaplan

GCC 9.2'de herhangi bir fark yok gibi görünmüyor: gcc.godbolt.org/z/GzP6cx (aslında, zaten 8.1'de)
Ruslan

40

Fikri __builtin_expect, derleyiciye genellikle ifadenin c olarak değerlendirildiğini bulacağınızı söylemektir, böylece derleyici bu durum için optimize edebilir.

Birisinin zeki olduklarını düşündüğünü ve bunu yaparak işleri hızlandırdığını tahmin ediyorum.

Ne yazık ki, durum çok iyi anlaşılmadıkça (muhtemelen böyle bir şey yapmadılar), işleri daha da kötüleştirmiş olabilir. Belgeler bile şöyle diyor:

Genel olarak, -fprofile-arcsprogramcılar programlarının gerçekte nasıl çalıştığını tahmin etmede kötü bir şekilde kötü olduğundan, bunun için gerçek profil geri bildirimini ( ) kullanmayı tercih etmelisiniz . Ancak, bu verilerin toplanmasının zor olduğu uygulamalar vardır.

Genel olarak, aşağıdaki durumlar dışında kullanmamalısınız __builtin_expect:

  • Çok gerçek bir performans sorununuz var
  • Sistemdeki algoritmaları zaten uygun şekilde optimize ettiniz
  • Belirli bir vakanın en olası olduğu iddianızı desteklemek için performans verileriniz var

7
@Michael: Bu gerçekten şube tahmininin bir tanımı değil.
Oliver Charlesworth

3
"Çoğu programcı KÖTÜ" ya da derleyiciden daha iyi değil. Herhangi bir salak bir for döngüsünde, devam koşulunun muhtemelen doğru olduğunu söyleyebilir, ancak derleyici bunu bilir, bu yüzden bunu söylemenin bir yararı yoktur. Nedense Eğer hemen her zaman hemen kıracağını bir döngü yazmış ve PGO için derleyiciye profil verilerini sağlayamaz ise, o zaman belki programcı derleyici değil bir şeyler biliyor.
Steve Jessop

15
Bazı durumlarda, hangi dalın daha olası olduğu önemli değil, daha çok hangi dalın önemli olduğu. Beklenmeyen dal durmaya () yol açarsa, olasılık önemli değildir ve optimizasyon sırasında beklenen dala performans önceliği verilmelidir.
Neowizard

1
Talebinizle ilgili sorun, CPU'nun dal olasılığı açısından gerçekleştirebileceği optimizasyonların bir taneyle sınırlı olmasıdır: dal tahmini ve bu optimizasyon__builtin_expect , kullansanız da kullanmasanız da gerçekleşir . Öte yandan, derleyici, kodun sıcak yolun bitişik olması için düzenlenmesi, kodun daha fazla optimize edilmesi olası olmayan bir şekilde hareket ettirilmesi veya boyutunun küçültülmesi, hangi dalların vektörleştirileceğine karar vermesi, şube olasılığına dayalı birçok optimizasyon gerçekleştirebilir, sıcak yolu daha iyi zamanlama, vb.
BeeOnRope

1
... geliştiricinin bilgisi olmadan kördür ve tarafsız bir strateji seçer. Geliştirici olasılıklar konusunda haklıysa (ve çoğu durumda bir dalın genellikle alındığını / alındığını anlamak önemsizdir) - bu avantajlardan faydalanırsınız. Eğer bir ceza almazsanız, ancak bir şekilde faydalardan çok daha büyük değildir ve en önemlisi, bunların hiçbiri bir şekilde CPU dalı tahminini geçersiz kılmaz .
BeeOnRope

13

Açıklamasında belirttiği gibi, ilk sürüm yapıya öngörücü bir unsur ekleyerek derleyiciye x == 0dalın daha muhtemel olduğunu söyler - yani, programınız tarafından daha sık alınacak daldır.

Bunu göz önünde bulundurarak, derleyici koşulu, beklenmedik durum durumunda belki de daha fazla iş yapmak zorunda kalacak pahasına, beklenen koşul beklediğinde en az miktarda çalışma gerektirecek şekilde optimize edebilir.

Bir dalın diğerinden nasıl daha az çalışabileceğini görmek için, derleme aşamasında ve sonuçta oluşan derlemede koşulların nasıl uygulandığına bir göz atın.

Bununla birlikte, bu optimizasyonun sadece söz konusu koşullu, çok fazla çağrılan sıkı bir iç döngü parçası olması durumunda fark edilebilir bir etkiye sahip olmasını beklerim , çünkü elde edilen koddaki fark nispeten küçüktür. Ve yanlış yönde optimize ederseniz, performansınızı düşürebilirsiniz.


Ama sonunda her şey derleyicinin durumunu kontrol etmekle ilgili, derleyicinin her zaman bu dalı üstlendiğini ve ilerlediğini ve daha sonra bir eşleşme yoksa demek istiyor musunuz? Ne oluyor? Derleyici tasarımında bu dal tahmini şeyleri ve nasıl çalıştığı hakkında daha fazla şey olduğunu düşünüyorum.
kingsmasher1

2
Bu gerçekten bir mikro optimizasyon. Koşulların nasıl uygulandığına bakın, bir şubeye karşı küçük bir önyargı var. Varsayımsal bir örnek olarak, bir koşulun bir test artı montajda bir sıçrama haline geldiğini varsayalım. Sonra atlama dalı atlamayan olandan daha yavaştır, bu nedenle beklenen dalı atlamayan olanı yapmayı tercih edersiniz.
Kerrek SB

Teşekkürler, senin ve Michael sanırım benzer görüşlere sahip ama farklı kelimeler koymak :-) Ben Test-ve-şube hakkında açıklamak mümkün değil tam derleyici iç anlamak mümkün değildir :)
kingsmasher1

İnternette arama yaparak da öğrenmeleri çok kolay :-)
Kerrek SB

Benim kolej kitabım compiler design - Aho, Ullmann, Sethi:-)
kingsmasher1

1

Sorduğunuzu, yorumladığınızı düşündüğüm soruya cevap veren hiçbir cevap görmüyorum:

Derleyiciye şube tahminini taşımanın daha taşınabilir bir yolu var mı?

Sorunuzun başlığı bana bu şekilde yaptığımı düşündürdü:

if ( !x ) {} else foo();

Derleyici 'true' değerinin daha olası olduğunu varsayarsa, çağırmamak için en iyi duruma getirebilir foo().

Buradaki sorun, genel olarak, derleyicinin ne alacağını bilmemenizdir - bu nedenle bu tür bir tekniği kullanan herhangi bir kodun dikkatlice ölçülmesi gerekir (ve bağlam değişirse muhtemelen zaman içinde izlenmelidir).


Bu aslında OP'nin başlangıçta yazmayı amaçladığı şey olabilir (başlık tarafından belirtildiği gibi) - ancak bir nedenden dolayı kullanımı elseyazı gövdesinin dışında bırakıldı.
Brent Bradburn

1

@Blagovest Buyukliev ve @Ciro'ya göre Mac'te test ediyorum. Gruplar net görünüyor ve yorum ekliyorum;

Komutlar gcc -c -O3 -std=gnu11 testOpt.c; otool -tVI testOpt.o

-O3 use kullandığımda __builtin_expect (i, 0) olursa olsun aynı görünmüyor.

testOpt.o:
(__TEXT,__text) section
_main:
0000000000000000    pushq   %rbp     
0000000000000001    movq    %rsp, %rbp    // open function stack
0000000000000004    xorl    %edi, %edi       // set time args 0 (NULL)
0000000000000006    callq   _time      // call time(NULL)
000000000000000b    testq   %rax, %rax   // check time(NULL)  result
000000000000000e    je  0x14           //  jump 0x14 if testq result = 0, namely jump to puts
0000000000000010    xorl    %eax, %eax   //  return 0   ,  return appear first 
0000000000000012    popq    %rbp    //  return 0
0000000000000013    retq                     //  return 0
0000000000000014    leaq    0x9(%rip), %rdi  ## literal pool for: "a"  // puts  part, afterwards
000000000000001b    callq   _puts
0000000000000020    xorl    %eax, %eax
0000000000000022    popq    %rbp
0000000000000023    retq

-O2 , ile derlendiğinde __builtin_expect ile (i, 0)

İlk olmadan

testOpt.o:
(__TEXT,__text) section
_main:
0000000000000000    pushq   %rbp
0000000000000001    movq    %rsp, %rbp
0000000000000004    xorl    %edi, %edi
0000000000000006    callq   _time
000000000000000b    testq   %rax, %rax
000000000000000e    jne 0x1c       //   jump to 0x1c if not zero, then return
0000000000000010    leaq    0x9(%rip), %rdi ## literal pool for: "a"   //   put part appear first ,  following   jne 0x1c
0000000000000017    callq   _puts
000000000000001c    xorl    %eax, %eax     // return part appear  afterwards
000000000000001e    popq    %rbp
000000000000001f    retq

Şimdi __builtin_expect ile (i, 0)

testOpt.o:
(__TEXT,__text) section
_main:
0000000000000000    pushq   %rbp
0000000000000001    movq    %rsp, %rbp
0000000000000004    xorl    %edi, %edi
0000000000000006    callq   _time
000000000000000b    testq   %rax, %rax
000000000000000e    je  0x14   // jump to 0x14 if zero  then put. otherwise return 
0000000000000010    xorl    %eax, %eax   // return appear first 
0000000000000012    popq    %rbp
0000000000000013    retq
0000000000000014    leaq    0x7(%rip), %rdi ## literal pool for: "a"
000000000000001b    callq   _puts
0000000000000020    jmp 0x10

Özetlemek gerekirse, __builtin_expect son durumda çalışır.

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.