Jesteś ofiarą niepowodzenia prognozowania gałęzi .
Co to jest przewidywanie gałęzi?
Rozważ węzeł kolejowy:
Zdjęcie Mecanismo, za pośrednictwem Wikimedia Commons. Używany na licencji CC-By-SA 3.0 .
Teraz, dla argumentu, załóżmy, że jest to już w 1800 roku - przed długą rozmową lub komunikacją radiową.
Jesteś operatorem skrzyżowania i słyszysz nadjeżdżający pociąg. Nie masz pojęcia, w którą stronę ma iść. Zatrzymujesz pociąg, aby zapytać kierowcę, który kierunek chce. A następnie odpowiednio ustawiłeś przełącznik.
Pociągi są ciężkie i mają dużą bezwładność. Więc zaczynają i zwalniają.
Czy jest lepszy sposób? Zgadnij, w którą stronę pójdzie pociąg!
- Jeśli dobrze zgadłeś, nadal trwa.
- Jeśli pomyliłeś się, kapitan zatrzyma się, cofnie i krzyknie na ciebie, aby przełączyć przełącznik. Następnie może ponownie uruchomić inną ścieżkę.
Jeśli dobrze zgadniesz za każdym razem , pociąg nigdy nie będzie musiał się zatrzymywać.
Jeśli zbyt często się mylicie , pociąg poświęci dużo czasu na zatrzymywanie się, tworzenie kopii zapasowych i restartowanie.
Rozważmy instrukcję if: na poziomie procesora jest to instrukcja rozgałęziona:
Jesteś procesorem i widzisz oddział. Nie masz pojęcia, w którą stronę pójdzie. Co robisz? Zatrzymujesz wykonywanie i czekasz, aż poprzednie instrukcje zostaną zakończone. Następnie idź właściwą ścieżką.
Nowoczesne procesory są skomplikowane i mają długie rurociągi. Dlatego trwają wiecznie, aby się „rozgrzać” i „zwolnić”.
Czy jest lepszy sposób? Zgadnij, w którą stronę pójdzie oddział!
- Jeśli dobrze zgadłeś, kontynuujesz wykonywanie.
- Jeśli pomyliłeś się, musisz przepłukać rurociąg i przetoczyć się z powrotem do gałęzi. Następnie możesz ponownie uruchomić inną ścieżkę.
Jeśli za każdym razem dobrze zgadniesz , egzekucja nigdy nie będzie musiała się kończyć.
Jeśli zbyt często się mylicie , spędzacie dużo czasu na zwlekaniu, wycofywaniu się i ponownym uruchamianiu.
To jest prognoza gałęzi. Przyznaję, że nie jest to najlepsza analogia, ponieważ pociąg może po prostu zasygnalizować kierunek flagą. Ale w komputerach procesor nie wie, w którą stronę pójdzie gałąź, do ostatniej chwili.
Jak więc strategicznie zgadnąć, aby zminimalizować liczbę przypadków, w których pociąg musi się wycofać i zejść inną drogą? Patrzysz na przeszłość! Jeśli pociąg jedzie w lewo w 99% przypadków, zgadujesz, że w lewo. Jeśli zmienia się, to na przemian zgadujesz. Jeśli pójdzie w jedną stronę co trzy razy, domyślacie się, że to samo ...
Innymi słowy, próbujesz zidentyfikować wzór i podążać za nim. Jest to mniej więcej sposób działania predyktorów gałęzi.
Większość aplikacji ma dobrze zachowujące się gałęzie. Tak więc nowoczesne predyktory branżowe zazwyczaj osiągają> 90% współczynników trafień. Ale w obliczu nieprzewidywalnych gałęzi bez rozpoznawalnych wzorców predyktory gałęzi są praktycznie bezużyteczne.
Dalsza lektura: Artykuł „Predyktor branży” na Wikipedii .
Jak wspomniano z góry, winowajcą jest to wyrażenie if:
if (data[c] >= 128)
sum += data[c];
Zauważ, że dane są równomiernie rozmieszczone między 0 a 255. Po posortowaniu danych, mniej więcej pierwsza połowa iteracji nie wejdzie w instrukcję if. Następnie wszyscy wprowadzą instrukcję if.
Jest to bardzo przyjazne dla predyktora gałęzi, ponieważ gałąź wielokrotnie podąża w tym samym kierunku wiele razy. Nawet prosty licznik nasycenia prawidłowo przewidzi gałąź, z wyjątkiem kilku iteracji po zmianie kierunku.
Szybka wizualizacja:
T = branch taken
N = branch not taken
data[] = 0, 1, 2, 3, 4, ... 126, 127, 128, 129, 130, ... 250, 251, 252, ...
branch = N N N N N ... N N T T T ... T T T ...
= NNNNNNNNNNNN ... NNNNNNNTTTTTTTTT ... TTTTTTTTTT (easy to predict)
Jednak gdy dane są całkowicie losowe, predyktor gałęzi staje się bezużyteczny, ponieważ nie może przewidzieć losowych danych. Zatem prawdopodobnie wystąpi około 50% nieprzewidywalności (nie lepiej niż losowe zgadywanie).
data[] = 226, 185, 125, 158, 198, 144, 217, 79, 202, 118, 14, 150, 177, 182, 133, ...
branch = T, T, N, T, T, T, T, N, T, N, N, T, T, T, N ...
= TTNTTTTNTNNTTTN ... (completely random - hard to predict)
Co więc można zrobić?
Jeśli kompilator nie jest w stanie zoptymalizować gałęzi do ruchu warunkowego, możesz spróbować kilku hacków, jeśli chcesz poświęcić czytelność wydajności.
Zastąpić:
if (data[c] >= 128)
sum += data[c];
z:
int t = (data[c] - 128) >> 31;
sum += ~t & data[c];
To eliminuje gałąź i zastępuje ją niektórymi operacjami bitowymi.
(Zauważ, że ten hack nie jest ściśle równoważny z oryginalną instrukcją if. W tym przypadku dotyczy wszystkich wartości wejściowych data[]
.)
Testy porównawcze: Core i7 920 @ 3,5 GHz
C ++ - Visual Studio 2010 - wydanie x64
// Branch - Random
seconds = 11.777
// Branch - Sorted
seconds = 2.352
// Branchless - Random
seconds = 2.564
// Branchless - Sorted
seconds = 2.587
Java - NetBeans 7.1.1 JDK 7 - x64
// Branch - Random
seconds = 10.93293813
// Branch - Sorted
seconds = 5.643797077
// Branchless - Random
seconds = 3.113581453
// Branchless - Sorted
seconds = 3.186068823
Obserwacje:
- Z odgałęzieniem: Istnieje ogromna różnica między posortowanymi i nieposortowanymi danymi.
- With Hack: Nie ma różnicy między posortowanymi i nieposortowanymi danymi.
- W przypadku C ++ włamanie jest odrobinę wolniejsze niż w przypadku gałęzi podczas sortowania danych.
Ogólna zasada polega na unikaniu rozgałęzień zależnych od danych w pętlach krytycznych (takich jak w tym przykładzie).
Aktualizacja:
GCC 4.6.1 z -O3
lub -ftree-vectorize
na x64 jest w stanie wygenerować ruch warunkowy. Nie ma więc różnicy między posortowanymi i nieposortowanymi danymi - oba są szybkie.
(Lub nieco szybciej: w przypadku już posortowanego przypadku cmov
może być wolniejszy, szczególnie jeśli GCC umieści go na ścieżce krytycznej zamiast po prostu add
, szczególnie na Intel przed Broadwell, gdzie cmov
ma 2 opóźnienia cyklu: flaga optymalizacji gcc -O3 powoduje, że kod jest wolniejszy niż -O2 )
VC ++ 2010 nie jest w stanie wygenerować ruchów warunkowych dla tej gałęzi, nawet pod /Ox
.
Kompilator Intel C ++ (ICC) 11 robi coś cudownego. To węzłów dwie pętle , a tym samym podnoszenia nieprzewidywalne odgałęzienie do zewnętrznej pętli. Jest więc nie tylko odporny na nieprzewidziane zdarzenia, ale także dwa razy szybszy niż cokolwiek, co generują VC ++ i GCC! Innymi słowy, ICC skorzystało z pętli testowej, aby pokonać punkt odniesienia ...
Jeśli podasz kompilatorowi Intela kod bez rozgałęzień, to po prostu wektoryzuje go ... i jest tak samo szybki jak w gałęzi (z wymianą pętli).
To pokazuje, że nawet dojrzałe współczesne kompilatory mogą się bardzo różnić w zakresie możliwości optymalizacji kodu ...