Natknąłem się na plik, #define
w którym używają __builtin_expect
.
Dokumentacja mówi:
Wbudowana funkcja:
long __builtin_expect (long exp, long c)
Możesz użyć,
__builtin_expect
aby dostarczyć kompilatorowi informacje przewidywania gałęzi. Ogólnie rzecz biorąc, do tego (-fprofile-arcs
) należy używać rzeczywistych opinii o profilu , ponieważ programiści są notorycznie słabi w przewidywaniu rzeczywistego działania ich programów. Są jednak aplikacje, w których trudno jest zebrać te dane.Wartość zwracana to wartość
exp
, która powinna być wyrażeniem całkowitym. Semantyka elementu wbudowanego polega na tym, że jest to oczekiwaneexp == c
. Na przykład:if (__builtin_expect (x, 0)) foo ();
wskazywałoby, że nie spodziewamy się zadzwonić
foo
, ponieważ spodziewamyx
się, że będzie zero.
Dlaczego więc nie użyć bezpośrednio:
if (x)
foo ();
zamiast skomplikowanej składni z __builtin_expect
?
if ( x == 0) {} else foo();
... lub po prostuif ( x != 0 ) foo();
odpowiednikiem kodu z dokumentacji GCC.Odpowiedzi:
Wyobraź sobie kod asemblera, który zostałby wygenerowany z:
Myślę, że powinno to być coś takiego:
Widać, że instrukcje są ułożone w takiej kolejności, że
bar
sprawa poprzedzafoo
sprawę (w przeciwieństwie do kodu C). Może to lepiej wykorzystywać potok procesora, ponieważ skok odrzuca już pobrane instrukcje.Przed wykonaniem skoku instrukcje znajdujące się pod nim (
bar
obudowa) są umieszczane w potoku. Ponieważfoo
sprawa jest mało prawdopodobna, skakanie również jest mało prawdopodobne, dlatego też jest mało prawdopodobne, aby uderzyć rurociąg.źródło
x = 0
więc słupek jest podawany jako pierwszy. I foo, jest zdefiniowane później, ponieważ jego szanse (raczej prawdopodobieństwo użycia) są mniejsze, prawda?Zdekompilujmy, aby zobaczyć, co robi z tym GCC 4.8
Blagovest wspomniał o inwersji gałęzi w celu ulepszenia potoku, ale czy obecne kompilatory naprawdę to robią? Dowiedzmy Się!
Bez
__builtin_expect
Kompiluj i dekompiluj z GCC 4.8.2 x86_64 Linux:
Wynik:
Kolejność instrukcji w pamięci pozostała niezmieniona: najpierw the,
puts
a następnieretq
powrót.Z
__builtin_expect
Teraz zamień na
if (i)
:i otrzymujemy:
puts
Została przeniesiona do samego końca funkcji, wretq
zamian!Nowy kod jest w zasadzie taki sam jak:
Ta optymalizacja nie została wykonana
-O0
.Ale powodzenia w pisaniu przykładu, który działa szybciej z procesorami
__builtin_expect
niż bez nich, w dzisiejszych czasach są naprawdę inteligentne . Moje naiwne próby są tutaj .C ++ 20
[[likely]]
i[[unlikely]]
C ++ 20 ustandaryzował te wbudowane C ++: Jak używać atrybutu prawdopodobny / mało prawdopodobny C ++ 20 w instrukcji if-else Prawdopodobnie (gra słów!) Zrobią to samo.
źródło
Pomysł
__builtin_expect
polega na tym, aby powiedzieć kompilatorowi, że zwykle okaże się, że wyrażenie ma wartość c, aby kompilator mógł zoptymalizować ten przypadek.Domyślam się, że ktoś myślał, że jest sprytny i że robiąc to, przyspiesza.
Niestety, jeśli sytuacja nie jest dobrze zrozumiana (prawdopodobnie nie zrobili czegoś takiego), mogło to pogorszyć sytuację. Dokumentacja mówi nawet:
Ogólnie rzecz biorąc, nie powinieneś używać,
__builtin_expect
chyba że:źródło
__builtin_expect
czy nie . Z drugiej strony kompilator może przeprowadzić wiele optymalizacji na podstawie prawdopodobieństwa rozgałęzienia, takich jak zorganizowanie kodu tak, aby gorąca ścieżka była ciągła, przenoszenie kodu prawdopodobnie nie było zoptymalizowane dalej lub zmniejszenie jego rozmiaru, podejmowanie decyzji o tym, które gałęzie mają być wektoryzowane, lepsze planowanie gorącej ścieżki i tak dalej.Cóż, jak jest napisane w opisie, pierwsza wersja dodaje do konstrukcji element predykcyjny, mówiąc kompilatorowi, że
x == 0
gałąź jest bardziej prawdopodobna - to znaczy jest to gałąź, która będzie częściej pobierana przez twój program.Mając to na uwadze, kompilator może zoptymalizować warunek tak, aby wymagał najmniejszego nakładu pracy, gdy jest spełniony oczekiwany warunek, kosztem być może konieczności wykonania większej ilości pracy w przypadku nieoczekiwanego stanu.
Przyjrzyj się, jak warunkowe są implementowane podczas fazy kompilacji, a także w wynikowym asemblacji, aby zobaczyć, jak jedna gałąź może być mniej obciążona niż druga.
Jednak spodziewałbym się, że ta optymalizacja przyniesie zauważalny efekt tylko wtedy, gdy dany warunek jest częścią ścisłej wewnętrznej pętli, która jest często wywoływana , ponieważ różnica w wynikowym kodzie jest stosunkowo niewielka. A jeśli zoptymalizujesz go w niewłaściwy sposób, możesz zmniejszyć wydajność.
źródło
compiler design - Aho, Ullmann, Sethi
:-)Nie widzę żadnej odpowiedzi odnoszącej się do pytania, o które myślę, że zadawałeś, parafrazując:
Tytuł twojego pytania skłonił mnie do zrobienia tego w ten sposób:
Jeśli kompilator przyjmie, że „prawda” jest bardziej prawdopodobna, może zoptymalizować, aby nie wywoływać
foo()
.Problem polega na tym, że generalnie nie wiesz, co przyjmie kompilator - więc każdy kod, który używa tego rodzaju techniki, musiałby być dokładnie zmierzony (i prawdopodobnie monitorowany w czasie, jeśli zmieni się kontekst).
źródło
else
zostało pominięte w treści postu.Testuję to na Macu zgodnie z @Blagovest Buyukliev i @Ciro. Zestawy wyglądają przejrzyście i dodaję komentarze;
Polecenia są
gcc -c -O3 -std=gnu11 testOpt.c; otool -tVI testOpt.o
Kiedy używam -O3 ,, wygląda to tak samo, niezależnie od tego, czy __builtin_expect (i, 0) istnieje, czy nie.
Przy kompilacji z -O2 , wygląda inaczej zi bez __builtin_expect (i, 0)
Po pierwsze bez
Teraz z __builtin_expect (i, 0)
Podsumowując, __builtin_expect działa w tym ostatnim przypadku.
źródło