Oto fragment kodu C ++, który pokazuje niektóre bardzo dziwne zachowania. Z jakiegoś dziwnego powodu sortowanie danych w cudowny sposób przyspiesza prawie sześciokrotnie:
#include <algorithm>
#include <ctime>
#include <iostream>
int main()
{
// Generate data
const unsigned arraySize = 32768;
int data[arraySize];
for (unsigned c = 0; c < arraySize; ++c)
data[c] = std::rand() % 256;
// !!! With this, the next loop runs faster.
std::sort(data, data + arraySize);
// Test
clock_t start = clock();
long long sum = 0;
for (unsigned i = 0; i < 100000; ++i)
{
// Primary loop
for (unsigned c = 0; c < arraySize; ++c)
{
if (data[c] >= 128)
sum += data[c];
}
}
double elapsedTime = static_cast<double>(clock() - start) / CLOCKS_PER_SEC;
std::cout << elapsedTime << std::endl;
std::cout << "sum = " << sum << std::endl;
}
- Bez
std::sort(data, data + arraySize);
tego kod działa w 11,54 sekundy. - Po posortowaniu danych kod działa w 1,93 sekundy.
Początkowo myślałem, że może to być anomalia dotycząca języka lub kompilatora, więc wypróbowałem Javę:
import java.util.Arrays;
import java.util.Random;
public class Main
{
public static void main(String[] args)
{
// Generate data
int arraySize = 32768;
int data[] = new int[arraySize];
Random rnd = new Random(0);
for (int c = 0; c < arraySize; ++c)
data[c] = rnd.nextInt() % 256;
// !!! With this, the next loop runs faster
Arrays.sort(data);
// Test
long start = System.nanoTime();
long sum = 0;
for (int i = 0; i < 100000; ++i)
{
// Primary loop
for (int c = 0; c < arraySize; ++c)
{
if (data[c] >= 128)
sum += data[c];
}
}
System.out.println((System.nanoTime() - start) / 1000000000.0);
System.out.println("sum = " + sum);
}
}
Z podobnym, ale mniej ekstremalnym rezultatem.
Najpierw pomyślałem, że sortowanie przenosi dane do pamięci podręcznej, ale potem pomyślałem, że to głupie, ponieważ tablica właśnie została wygenerowana.
- Co się dzieje?
- Dlaczego przetwarzanie posortowanej tablicy jest szybsze niż przetwarzanie nieposortowanej tablicy?
Kod sumuje niektóre niezależne warunki, więc kolejność nie powinna mieć znaczenia.
java
c++
performance
optimization
branch-prediction
GManNickG
źródło
źródło
Odpowiedzi:
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 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 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:
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:
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).
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ć:
z:
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
Java - NetBeans 7.1.1 JDK 7 - x64
Obserwacje:
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 prostuadd
, szczególnie na Intel przed Broadwell, gdziecmov
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 ...
źródło
Prognozowanie gałęzi.
W przypadku posortowanej tablicy warunek
data[c] >= 128
jest najpierwfalse
dla pasma wartości, a następnietrue
dla wszystkich późniejszych wartości. Łatwo to przewidzieć. W przypadku nieposortowanej tablicy płacisz za koszty rozgałęzienia.źródło
Powodem, dla którego wydajność drastycznie poprawia się podczas sortowania danych, jest usunięcie kary przewidywania gałęzi, jak pięknie wyjaśniono w odpowiedzi Mysticial .
Teraz, jeśli spojrzymy na kod
możemy stwierdzić, że znaczenie tej konkretnej
if... else...
gałęzi jest dodanie czegoś, gdy warunek jest spełniony. Ten typ oddziału można łatwo przekształcić w instrukcję warunkowego przeniesienia , która zostałaby skompilowana w instrukcję warunkowego przeniesienia:cmovl
wx86
systemie. Gałąź, a tym samym potencjalna kara za przewidywanie gałęzi, jest usuwana.W
C
ten sposóbC++
, oświadczenie, które skompilować bezpośrednio (bez optymalizacji) w instrukcji warunkowej poruszać sięx86
, jest operatorem trójskładnikowych... ? ... : ...
. Więc przepisujemy powyższą instrukcję na równoważną:Zachowując czytelność, możemy sprawdzić współczynnik przyspieszenia.
Na procesorze Intel Core i7 -2600K @ 3,4 GHz i Visual Studio 2010 w wersji testowej test porównawczy (format skopiowany z Mysticial):
x86
x64
Wynik jest solidny w wielu testach. Dostajemy duże przyspieszenie, gdy wynik gałęzi jest nieprzewidywalny, ale cierpimy trochę, gdy jest przewidywalny. W rzeczywistości podczas korzystania z ruchu warunkowego wydajność jest taka sama, niezależnie od wzorca danych.
Przyjrzyjmy się teraz bliżej badając
x86
zespół, który generują. Dla uproszczenia używamy dwóch funkcjimax1
imax2
.max1
używa gałęzi warunkowejif... else ...
:max2
używa operatora trójskładnikowego... ? ... : ...
:Na maszynie x86-64
GCC -S
generuje zestaw poniżej.max2
zużywa znacznie mniej kodu ze względu na użycie instrukcjicmovge
. Ale prawdziwym zyskiem jest to, żemax2
nie obejmuje skoków gałęzi,jmp
, co miałoby znaczną karę wydajności, jeśli przewidywany wynik byłby niewłaściwy.Dlaczego więc ruch warunkowy działa lepiej?
W typowym
x86
procesorze wykonanie instrukcji jest podzielone na kilka etapów. Z grubsza mamy inny sprzęt do obsługi różnych etapów. Nie musimy więc czekać na zakończenie jednej instrukcji, aby rozpocząć nową. Nazywa się to potokowaniem .W przypadku rozgałęzienia następująca instrukcja jest określana przez poprzednią, więc nie możemy wykonywać potokowania. Musimy albo poczekać, albo przewidzieć.
W przypadku warunkowego ruchu instrukcja warunkowego wykonania wykonania jest podzielona na kilka etapów, ale wcześniejsze etapy, takie jak
Fetch
iDecode
nie zależą od wyniku poprzedniej instrukcji; tylko ostatnie etapy potrzebują rezultatu. Dlatego czekamy ułamek czasu wykonania jednej instrukcji. Właśnie dlatego wersja warunkowego przenoszenia jest wolniejsza niż gałąź, gdy przewidywanie jest łatwe.Książka Computer Systems: A Programmer's Perspective, drugie wydanie , szczegółowo to wyjaśnia. Możesz zapoznać się z sekcją 3.6.6, aby uzyskać instrukcje dotyczące warunkowego przenoszenia , cały rozdział 4 dotyczący architektury procesora , a sekcję 5.11.2, aby uzyskać specjalne informacje na temat kar przewidujących rozgałęzienia i niedopuszczalności .
Czasami niektóre nowoczesne kompilatory mogą zoptymalizować nasz kod do złożenia z lepszą wydajnością, czasem niektóre kompilatory nie mogą (dany kod używa natywnego kompilatora Visual Studio). Znajomość różnicy w wydajności między oddziałem a ruchem warunkowym, gdy jest nieprzewidywalny, może pomóc nam pisać kod z lepszą wydajnością, gdy scenariusz staje się tak skomplikowany, że kompilator nie może ich automatycznie zoptymalizować.
źródło
-O0
przykład i pokazać różnicę w zoptymalizowanym asmie na twoich dwóch testach.Jeśli jesteś ciekawy jeszcze większej optymalizacji tego kodu, rozważ to:
Zaczynając od oryginalnej pętli:
Dzięki wymianie pętli możemy bezpiecznie zmienić tę pętlę na:
Następnie możesz zobaczyć, że
if
warunek jest stały podczas wykonywaniai
pętli, więc możeszif
wyciągnąć:Następnie zobaczysz, że pętla wewnętrzna może zostać zwinięta w jedno wyrażenie, przy założeniu, że zezwala na to model zmiennoprzecinkowy (
/fp:fast
na przykład jest rzucany)Ten jest 100 000 razy szybszy niż wcześniej.
źródło
i
jedną jednostkę = 1e5. Nie ma to znaczenia dla wyniku końcowego, ale chciałem po prostu ustawić rekord, ponieważ jest to tak często odwiedzana strona.if
w tym miejscu można przekonwertować na:sum += (data[j] >= 128) ? data[j] * 100000 : 0;
który kompilator może być w stanie zredukować docmovge
lub równoważny.Bez wątpienia niektórzy z nas byliby zainteresowani sposobami identyfikacji kodu, który jest problematyczny dla predyktora gałęzi procesora. Narzędzie Valgrind
cachegrind
ma symulator predykcji gałęzi, włączony za pomocą--branch-sim=yes
flagi. Uruchomienie go na przykładach w tym pytaniu, z liczbą zewnętrznych pętli zmniejszoną do 10000 i skompilowaną zg++
, daje następujące wyniki:Posortowane:
Nieposortowany:
Przechodząc do wyjścia produkowanego przez linię po linii
cg_annotate
, widzimy dla danej pętli:Posortowane:
Nieposortowany:
To pozwala łatwo zidentyfikować problematyczną linię - w nieposortowanej wersji
if (data[c] >= 128)
linia powoduje 164.050,007 nieprzewidzianych rozgałęzień warunkowych (Bcm
) w modelu predykcyjnym rozgałęzienia cachegrinda, podczas gdy powoduje tylko 10,006 w posortowanej wersji.Alternatywnie w systemie Linux można skorzystać z podsystemu liczników wydajności, aby wykonać to samo zadanie, ale z wydajnością natywną przy użyciu liczników procesora.
Posortowane:
Nieposortowany:
Może także wykonywać adnotacje do kodu źródłowego z dezasemblacją.
Zobacz samouczek wydajności, aby uzyskać więcej informacji.
źródło
data[c] >= 128
(który ma 50% wskaźnika opóźnień, jak sugerujesz) i jeden dla warunku pętli,c < arraySize
który ma ~ 0% wskaźnika opóźnień .Właśnie przeczytałem to pytanie i jego odpowiedzi i czuję, że brakuje odpowiedzi.
Powszechnym sposobem na wyeliminowanie przewidywania gałęzi, które okazało się szczególnie dobre w językach zarządzanych, jest wyszukiwanie tabel zamiast korzystania z gałęzi (chociaż nie testowałem tego w tym przypadku).
To podejście działa ogólnie, jeśli:
Tło i dlaczego
Z perspektywy procesora pamięć jest wolna. Aby zrekompensować różnicę prędkości, w twoim procesorze wbudowanych jest kilka pamięci podręcznych (pamięć podręczna L1 / L2). Wyobraź sobie, że wykonujesz swoje miłe obliczenia i zorientuj się, że potrzebujesz pamięci. Procesor otrzyma operację „ładowania” i ładuje pamięć do pamięci podręcznej - a następnie wykorzystuje pamięć podręczną do wykonania pozostałych obliczeń. Ponieważ pamięć jest stosunkowo wolna, to „ładowanie” spowolni twój program.
Podobnie jak przewidywanie gałęzi, zostało to zoptymalizowane w procesorach Pentium: procesor przewiduje, że musi załadować kawałek danych i próbuje załadować to do pamięci podręcznej, zanim operacja rzeczywiście trafi do pamięci podręcznej. Jak już widzieliśmy, przewidywanie rozgałęzień czasami idzie strasznie źle - w najgorszym przypadku musisz cofnąć się i faktycznie czekać na obciążenie pamięci, które potrwa wieczność ( innymi słowy: niepoprawne przewidywanie rozgałęzienia jest złe, pamięć obciążenie po niepowodzeniu przewidywania gałęzi jest po prostu okropne! ).
Na szczęście dla nas, jeśli wzorzec dostępu do pamięci jest przewidywalny, procesor załaduje go do szybkiej pamięci podręcznej i wszystko będzie dobrze.
Pierwszą rzeczą, którą musimy wiedzieć, jest to, co jest małe ? Podczas gdy mniejsze jest ogólnie lepsze, ogólną zasadą jest trzymanie się tablic odnośników o rozmiarze <= 4096 bajtów. Jako górny limit: jeśli twoja tabela odnośników jest większa niż 64 KB, prawdopodobnie warto ją ponownie rozważyć.
Konstruowanie stołu
Odkryliśmy więc, że możemy stworzyć mały stolik. Następnie należy uruchomić funkcję wyszukiwania. Funkcje wyszukiwania są zwykle małymi funkcjami, które wykorzystują kilka podstawowych operacji na liczbach całkowitych (i, lub xor, shift, dodawanie, usuwanie i być może mnożenie). Chcesz, aby twoje dane wejściowe zostały przetłumaczone przez funkcję wyszukiwania na jakiś „unikalny klucz” w twojej tabeli, który następnie po prostu daje odpowiedź na całą pracę, którą chciałeś wykonać.
W tym przypadku:> = 128 oznacza, że możemy zachować wartość, <128 oznacza, że się go pozbyliśmy. Najłatwiejszym sposobem na to jest użycie „AND”: jeśli je zachowamy, my AND to z 7FFFFFFF; jeśli chcemy się go pozbyć, my ORAZ 0. 0. Zauważ też, że 128 to potęga 2 - więc możemy iść do przodu i zrobić tabelę liczb całkowitych 32768/128 i wypełnić ją jednym zerem i dużą liczbą 7FFFFFFFF's.
Zarządzane języki
Możesz się zastanawiać, dlaczego działa to dobrze w zarządzanych językach. W końcu zarządzane języki sprawdzają granice tablic za pomocą gałęzi, aby upewnić się, że nie będziesz bałaganu ...
Cóż, niezupełnie ... :-)
Było sporo pracy nad wyeliminowaniem tej gałęzi dla języków zarządzanych. Na przykład:
W takim przypadku dla kompilatora oczywiste jest, że warunek brzegowy nigdy nie zostanie osiągnięty. Przynajmniej kompilator Microsoft JIT (ale spodziewam się, że Java robi podobne rzeczy) zauważy to i całkowicie usunie zaznaczenie. WOW, to oznacza brak oddziału. Podobnie będzie zajmować się innymi oczywistymi przypadkami.
Jeśli napotkasz problemy z przeglądaniem w zarządzanych językach - kluczem jest dodanie
& 0x[something]FFF
do funkcji wyszukiwania, aby umożliwić przewidywalne sprawdzenie granicy - i obserwowanie, jak przebiega szybciej.Wynik tego przypadku
źródło
sum += lookup[data[j]]
, gdzielookup
jest tablica z 256 wpisów, pierwsze z nich to zero, a te ostatnie są równe do indeksu?Ponieważ dane są rozdzielane między 0 a 255 podczas sortowania tablicy, mniej więcej w pierwszej połowie iteracji nie pojawi się
if
-statement (if
instrukcja jest udostępniana poniżej).Pytanie brzmi: co powoduje, że powyższe stwierdzenie nie jest wykonywane w niektórych przypadkach, jak w przypadku danych posortowanych? Oto „predyktor gałęzi”. Predyktor gałęzi to obwód cyfrowy, który próbuje odgadnąć, w którą stronę
if-then-else
pójdzie gałąź (np. Struktura), zanim zostanie to z całą pewnością znane. Celem predyktora rozgałęzienia jest poprawa przepływu w potoku instrukcji. Predyktory branżowe odgrywają kluczową rolę w osiągnięciu wysokiej wydajności!Zróbmy trochę benchmarkingu, aby lepiej to zrozumieć
Wydajność
if
-statement zależy od tego, czy jego stan ma przewidywalny wzorzec. Jeśli warunek jest zawsze prawdziwy lub zawsze fałszywy, logika przewidywania gałęzi w procesorze odbierze wzorzec. Z drugiej strony, jeśli wzór jest nieprzewidywalny, to stwierdzenieif
będzie znacznie droższe.Zmierzmy wydajność tej pętli w różnych warunkach:
Oto czasy pętli z różnymi wzorcami prawda-fałsz:
„ Zły ” wzór prawda-fałsz może stworzyć
if
będzie sześć razy wolniejsze niż „ dobry ” wzór! Oczywiście, który wzorzec jest dobry, a który zły, zależy od dokładnych instrukcji generowanych przez kompilator i od konkretnego procesora.Nie ma więc wątpliwości co do wpływu przewidywania gałęzi na wydajność!
źródło
Jednym ze sposobów uniknięcia błędów prognozowania gałęzi jest zbudowanie tabeli odnośników i zindeksowanie jej przy użyciu danych. Stefan de Bruijn omówił to w swojej odpowiedzi.
Ale w tym przypadku wiemy, że wartości mieszczą się w zakresie [0, 255] i dbamy tylko o wartości> = 128. Oznacza to, że możemy łatwo wyodrębnić pojedynczy bit, który powie nam, czy chcemy wartość, czy nie: poprzez przesunięcie dane w prawych 7 bitach, mamy 0 bitów lub 1 bitów i chcemy dodać wartość tylko wtedy, gdy mamy 1 bit. Nazwijmy ten bit „bitem decyzyjnym”.
Używając wartości 0/1 bitu decyzyjnego jako indeksu w tablicy, możemy stworzyć kod, który będzie równie szybki, niezależnie od tego, czy dane zostaną posortowane, czy nie. Nasz kod zawsze doda wartość, ale gdy bit decyzyjny ma wartość 0, dodamy wartość w miejscu, w którym nas nie obchodzi. Oto kod:
Ten kod marnuje połowę wartości dodanych, ale nigdy nie występuje błąd przewidywania gałęzi. Jest losowo szybszy w przypadku danych losowych niż wersja z rzeczywistą instrukcją if.
Ale w moich testach jawna tabela odnośników była nieco szybsza niż ta, prawdopodobnie dlatego, że indeksowanie do tabeli odnośników było nieco szybsze niż przesuwanie bitów. To pokazuje, jak mój kod konfiguruje się i korzysta z tabeli odnośników (niewyobrażalnie nazywanej
lut
w tabeli „LookUp Table”). Oto kod C ++:W tym przypadku tablica przeglądowa miała tylko 256 bajtów, więc ładnie mieści się w pamięci podręcznej i wszystko było szybkie. Ta technika nie działałaby dobrze, gdyby dane były 24-bitowymi wartościami, a chcieliśmy tylko połowy z nich ... tabela przeglądowa byłaby o wiele za duża, aby była praktyczna. Z drugiej strony możemy połączyć dwie techniki pokazane powyżej: najpierw przesuń bity, a następnie zindeksuj tabelę wyszukiwania. W przypadku 24-bitowej wartości, której potrzebujemy tylko górnej połowy, możemy potencjalnie przesunąć dane w prawo o 12 bitów i pozostawić 12-bitową wartość dla indeksu tabeli. 12-bitowy indeks tabeli implikuje tabelę 4096 wartości, co może być praktyczne.
Technika indeksowania do tablicy zamiast użycia
if
instrukcji może być użyta do podjęcia decyzji, którego wskaźnika użyć. Widziałem bibliotekę, w której zaimplementowano drzewa binarne, i zamiast dwóch nazwanych wskaźników (pLeft
i tak dalejpRight
) posiadał tablicę wskaźników o długości 2 i użyłem techniki „bitu decyzyjnego”, aby zdecydować, który wybrać. Na przykład zamiast:ta biblioteka zrobiłaby coś takiego:
Oto link do tego kodu: Red Black Trees , Eternally Confuzzled
źródło
data[c]>>7
- o czym również tutaj dyskutujemy); Celowo zrezygnowałem z tego rozwiązania, ale oczywiście masz rację. Tylko mała uwaga: podstawową zasadą dla tabel odnośników jest to, że jeśli pasuje do 4KB (z powodu buforowania), będzie działać - najlepiej sprawi, że tabela będzie jak najmniejsza. W przypadku języków zarządzanych przesunęłbym to do 64 KB, w przypadku języków niskiego poziomu, takich jak C ++ i C, prawdopodobnie ponownie się zastanowię (to tylko moje doświadczenie). Odtypeof(int) = 4
tego czasu staram się trzymać maksymalnie 10 bitów.sizeof(int) == 4
? Tak byłoby w przypadku wersji 32-bitowej. Mój dwuletni telefon komórkowy ma pamięć podręczną L1 o pojemności 32 KB, więc nawet tabela odnośników 4K może działać, szczególnie jeśli wartości odnośników byłyby bajtem zamiast int.j
metodzie równej 0 lub 1, dlaczego nie pomnożysz swojej wartościj
przed dodaniem jej zamiast korzystania z indeksowania tablic (być może powinno się ją pomnożyć1-j
zamiastj
)int c = data[j]; sum += c & -(c >> 7);
wymagałaby mnożenia.W posortowanym przypadku możesz zrobić coś lepszego niż poleganie na udanej prognozie gałęzi lub jakiejkolwiek sztuczce porównania bez gałęzi: całkowicie usuń gałąź.
Rzeczywiście, tablica jest podzielona na strefy ciągłe z
data < 128
i inną zdata >= 128
. Więc powinieneś znaleźć punkt podziału z wyszukiwaniem dychotomicznym (używającLg(arraySize) = 15
porównań), a następnie dokonać prostej akumulacji od tego punktu.Coś jak (niezaznaczone)
lub nieco bardziej zaciemnione
Jeszcze szybszym podejściem, które daje przybliżone rozwiązanie zarówno dla posortowanych, jak i nieposortowanych, jest:
sum= 3137536;
(zakładając naprawdę jednolity rozkład, 16384 próbek o oczekiwanej wartości 191,5) :-)źródło
sum= 3137536
- sprytny. To oczywiście nie o to chodzi. Pytanie wyraźnie dotyczy wyjaśnienia zaskakujących cech wydajności. Skłaniam się do stwierdzenia, że dodanie działaniastd::partition
zamiast zamiaststd::sort
jest cenne. Chociaż rzeczywiste pytanie dotyczy nie tylko podanego testu syntetycznego.Powyższe zachowanie występuje z powodu przewidywania gałęzi.
Aby zrozumieć przewidywanie gałęzi, należy najpierw zrozumieć potok instrukcji :
Każda instrukcja jest podzielona na sekwencję kroków, aby różne kroki mogły być wykonywane równolegle równolegle. Ta technika jest znana jako potok instrukcji i służy do zwiększenia przepustowości w nowoczesnych procesorach. Aby to lepiej zrozumieć, zobacz ten przykład na Wikipedii .
Ogólnie rzecz biorąc, nowoczesne procesory mają dość długie rurociągi, ale dla ułatwienia rozważmy tylko te 4 kroki.
4-etapowy rurociąg ogólnie dla 2 instrukcji.
Wracając do powyższego pytania, rozważmy następujące instrukcje:
Bez przewidywania gałęzi wystąpiłyby:
Aby wykonać instrukcję B lub instrukcję C, procesor będzie musiał poczekać, aż instrukcja A nie dojdzie do etapu EX w potoku, ponieważ decyzja o przejściu do instrukcji B lub instrukcji C zależy od wyniku instrukcji A. Tak więc potok będzie tak wyglądać.
kiedy, jeśli warunek zwraca wartość true:
Kiedy jeśli warunek zwraca false:
W wyniku oczekiwania na wynik instrukcji A łączna liczba cykli procesora spędzonych w powyższym przypadku (bez przewidywania gałęzi; zarówno dla prawdy, jak i fałszu) wynosi 7.
Czym jest prognoza gałęzi?
Narzędzie prognozy rozgałęzień spróbuje odgadnąć, w którą stronę pójdzie gałąź (struktura „jeśli-to-inaczej”), zanim będzie to pewne. Nie będzie czekać, aż instrukcja A osiągnie etap EX potoku, ale zgadnie decyzję i przejdzie do tej instrukcji (B lub C w przypadku naszego przykładu).
W przypadku prawidłowego odgadnięcia potok wygląda mniej więcej tak:
Jeśli później zostanie wykryte, że zgadnięcie było błędne, wówczas częściowo wykonane instrukcje są odrzucane, a potok zaczyna od nowa z prawidłową gałęzią, co powoduje opóźnienie. Czas marnowany w przypadku nieprzewidzianego rozgałęzienia jest równy liczbie etapów w potoku od etapu pobierania do etapu wykonywania. Współczesne mikroprocesory mają zwykle dość długie rurociągi, tak że opóźnienie w nieprzewidywalności wynosi od 10 do 20 cykli zegara. Im dłuższy rurociąg, tym większa potrzeba dobrego predyktora gałęzi .
W kodzie OP, po raz pierwszy, gdy warunkowy, predyktor gałęzi nie ma żadnych informacji umożliwiających przewidywanie, więc za pierwszym razem losowo wybierze następną instrukcję. Później w pętli for może opierać prognozy na historii. Dla tablicy posortowanej w porządku rosnącym istnieją trzy możliwości:
Załóżmy, że predyktor zawsze przyjmuje prawdziwą gałąź przy pierwszym uruchomieniu.
Tak więc w pierwszym przypadku zawsze przyjmie on prawdziwą gałąź, ponieważ historycznie wszystkie jej przewidywania są poprawne. W drugim przypadku początkowo będzie to przewidywać źle, ale po kilku iteracjach będzie poprawnie przewidywać. W trzecim przypadku będzie początkowo poprawnie przewidywał, aż elementy będą mniejsze niż 128. Po tym czasie zawiedzie przez pewien czas i poprawi się, gdy zobaczy awarię przewidywania gałęzi w historii.
We wszystkich tych przypadkach liczba awarii będzie zbyt mała, w wyniku czego tylko kilka razy będzie trzeba odrzucić częściowo wykonane instrukcje i zacząć od nowa z prawidłową gałęzią, co spowoduje mniej cykli procesora.
Ale w przypadku losowej nieposortowanej tablicy przewidywanie będzie musiało odrzucić częściowo wykonane instrukcje i zacząć od nowa z prawidłową gałęzią przez większość czasu i spowodować więcej cykli procesora w porównaniu do sortowanej tablicy.
źródło
Oficjalna odpowiedź pochodzi od
Na tym uroczym diagramie możesz także zobaczyć, dlaczego predyktor gałęzi jest zdezorientowany.
Każdy element w oryginalnym kodzie jest wartością losową
więc predyktor zmieni strony jako
std::rand()
cios.Z drugiej strony, po posortowaniu, predyktor najpierw przejdzie w stan silnie nieprzyjęty, a gdy wartości zmienią się na wysoką, predyktor w trzech biegnie przez całą zmianę od całkowicie nieprzyjętej do silnie pobranej.
źródło
W tym samym wierszu (myślę, że żadna odpowiedź tego nie podkreśliła) warto wspomnieć, że czasami (szczególnie w oprogramowaniu, w którym wydajność ma znaczenie - jak w jądrze Linuksa), można znaleźć następujące instrukcje, takie jak:
lub podobnie:
Zarówno
likely()
iunlikely()
są w rzeczywistości makr, które są zdefiniowane za pomocą coś jak GCC__builtin_expect
pomóc kompilator wstawianie kodu predykcji faworyzować podejmowania Stan techniczny pod uwagę informacje dostarczone przez użytkownika. GCC obsługuje inne wbudowane funkcje, które mogą zmieniać zachowanie działającego programu lub emitować instrukcje niskiego poziomu, takie jak czyszczenie pamięci podręcznej itp. Zobacz dokumentację zawierającą dostępne wbudowane funkcje GCC.Zazwyczaj tego rodzaju optymalizacje występują głównie w aplikacjach czasu rzeczywistego lub systemach wbudowanych, w których czas wykonania ma znaczenie i jest krytyczny. Na przykład, jeśli sprawdzasz, czy występuje jakiś błąd, który zdarza się tylko 1/10000000 razy, to dlaczego nie poinformować o tym kompilatora? W ten sposób domyślnie przewidywanie gałęzi zakłada, że warunek jest fałszywy.
źródło
Często używane operacje logiczne w C ++ tworzą wiele gałęzi w skompilowanym programie. Jeśli te gałęzie znajdują się w pętli i trudno je przewidzieć, mogą znacznie spowolnić wykonywanie. Zmienne boolowskie są przechowywane jako 8-bitowe liczby całkowite o wartościach
0
forfalse
i1
fortrue
.Zmienne boolowskie są nadmiernie określone w tym sensie, że wszystkie operatory, które mają zmienne boolowskie jako dane wejściowe, sprawdzają, czy dane wejściowe mają inną wartość niż
0
lub1
, ale operatory, które mają dane wyjściowe boolean , nie mogą generować innych wartości niż0
lub1
. To sprawia, że operacje na zmiennych logicznych jako danych wejściowych są mniej wydajne niż to konieczne. Rozważ przykład:Zwykle jest to realizowane przez kompilator w następujący sposób:
Ten kod jest daleki od optymalnego. Oddziały mogą trwać długo w przypadku nieprzewidzianych zdarzeń. Operacje boolowskie można uczynić znacznie wydajniejszymi, jeśli wiadomo z całą pewnością, że operandy nie mają innych wartości niż
0
i1
. Powodem, dla którego kompilator nie przyjmuje takiego założenia, jest to, że zmienne mogą mieć inne wartości, jeśli są niezainicjowane lub pochodzą z nieznanych źródeł. Powyższy kod można zoptymalizować jeślia
ib
został zainicjowany do prawidłowych wartości lub jeśli pochodzą one od podmiotów, które produkują wyjście Boolean. Zoptymalizowany kod wygląda następująco:char
jest używany zamiastbool
, aby umożliwić użycie operatorów bitowych (&
i|
) zamiast operatorów boolowskich (&&
i||
). Operatory bitowe są pojedynczymi instrukcjami, które biorą tylko jeden cykl zegara. Operator OR (|
) działa nawet jeślia
ib
mieć inne wartości niż0
lub1
. Operator AND (&
) i operator EXCLUSIVE OR (^
) mogą dawać niespójne wyniki, jeśli operandy mają inne wartości niż0
i1
.~
nie można użyć do NIE. Zamiast tego możesz utworzyć wartość logiczną NIE dla zmiennej, która jest znana,0
lub1
poprzez XOR'owanie jej za pomocą1
:można zoptymalizować w celu:
a && b
nie można zastąpića & b
ifb
jest wyrażeniem, którego nie należy oceniać, jeślia
jestfalse
(&&
nie będzieb
,&
będzie). Podobniea || b
nie może być zastąpionya | b
jeślib
to wyrażenie, które nie powinny być oceniane, czya
jesttrue
.Używanie operatorów bitowych jest bardziej korzystne, jeśli operandy są zmienne, niż jeśli operandy są porównaniami:
jest optymalna w większości przypadków (chyba że spodziewane jest, że
&&
wyrażenie wygeneruje wiele nieprzewidywalnych oddziałów).źródło
Na pewno!...
Prognozowanie gałęzi spowalnia logikę z powodu przełączania, które ma miejsce w kodzie! To tak, jakbyś wybierał prostą ulicę lub ulicę z wieloma zakrętami, na pewno prosta będzie szybsza! ...
Jeśli tablica jest posortowana, warunek jest fałszywy na pierwszym etapie:
data[c] >= 128
:, a następnie staje się prawdziwą wartością dla całej drogi do końca ulicy. W ten sposób szybciej dochodzisz do końca logiki. Z drugiej strony, używając nieposortowanej tablicy, potrzebujesz dużo obracania i przetwarzania, które z pewnością spowolnią działanie twojego kodu ...Spójrz na zdjęcie, które dla ciebie stworzyłem poniżej. Która ulica zostanie ukończona szybciej?
Więc programowo, przewidywanie gałęzi powoduje spowolnienie procesu ...
Na koniec warto wiedzieć, że mamy dwa rodzaje przewidywania gałęzi, z których każdy będzie miał inny wpływ na kod:
1. Statyczny
2. Dynamiczny
źródło
Odpowiedź na to pytanie była już wielokrotnie doskonała. Nadal chciałbym zwrócić uwagę grupy na kolejną interesującą analizę.
Ostatnio ten przykład (bardzo nieznacznie zmodyfikowany) został również użyty jako sposób na zademonstrowanie, jak kawałek kodu można profilować w samym programie w systemie Windows. Po drodze autor pokazuje również, jak wykorzystać wyniki, aby określić, gdzie kod spędza większość czasu zarówno w przypadku posortowanym, jak i nieposortowanym. Na koniec utwór pokazuje także, jak używać mało znanej funkcji warstwy HAL (Hardware Abstraction Layer), aby określić, jak często nieprzewidywalne są rozgałęzienia w nieposortowanym przypadku.
Link jest tutaj: http://www.geoffchappell.com/studies/windows/km/ntoskrnl/api/ex/profile/demo.htm
źródło
When the input is unsorted, all the rest of the loop takes substantial time. But with sorted input, the processor is somehow able to spend not just less time in the body of the loop, meaning the buckets at offsets 0x18 and 0x1C, but vanishingly little time on the mechanism of looping.
Autor próbuje omówić profilowanie w kontekście kodu zamieszczonego tutaj i podczas procesu próbuje wyjaśnić, dlaczego posortowana sprawa jest o wiele szybsza.Jak już wspomnieli inni, tajemnicą jest Predyktor gałęzi .
Nie próbuję niczego dodawać, ale wyjaśniam koncepcję w inny sposób. Na wiki znajduje się zwięzłe wprowadzenie, które zawiera tekst i schemat. Podobają mi się poniższe wyjaśnienia, które wykorzystują diagram do intuicyjnego opracowania Predictor gałęzi.
W oparciu o opisany scenariusz napisałem demo animacji, aby pokazać, w jaki sposób instrukcje są wykonywane w potoku w różnych sytuacjach.
Przykład zawiera trzy instrukcje, a pierwsza jest instrukcją skoku warunkowego. Dwie ostatnie instrukcje mogą przejść do potoku, dopóki nie zostanie wykonana instrukcja skoku warunkowego.
Wykonanie 3 instrukcji zajmie 9 cykli zegara.
Wykonanie 3 instrukcji zajmie 7 cykli zegara.
Wykonanie 3 instrukcji zajmie 9 cykli zegara.
Jak widać, wydaje się, że nie mamy powodu, aby nie używać programu Predictor gałęzi.
Jest to dość proste demo, które wyjaśnia bardzo podstawową część programu Predictor oddziału. Jeśli te gify są denerwujące, możesz je usunąć z odpowiedzi, a odwiedzający mogą również uzyskać kod źródłowy demonstracji na żywo z BranchPredictorDemo
źródło
if()
bloku może zostać wykonany, zanim stan rozgałęzienia zostanie rozpoznany . Lub dla pętli wyszukiwania, takiej jakstrlen
lubmemchr
, interakcje mogą się nakładać. Jeśli musiałbyś poczekać, aż wynik dopasowania będzie znany, zanim uruchomisz dowolną następną iterację, wąskie gardło będzie związane z ładowaniem pamięci podręcznej + opóźnieniem ALU zamiast przepustowości.Przewidywanie gałęzi!
Ważne jest, aby zrozumieć, że nieprzewidywalność gałęzi nie spowalnia programów. Koszt pominiętej prognozy jest taki, jakby przewidywanie gałęzi nie istniało, a użytkownik czekał na ocenę wyrażenia, aby zdecydować, który kod ma zostać uruchomiony (dalsze wyjaśnienia w następnym akapicie).
Ilekroć występuje instrukcja
if-else
\switch
, wyrażenie musi zostać ocenione w celu ustalenia, który blok powinien zostać wykonany. W kodzie zestawu generowanym przez kompilator wstawiane są instrukcje gałęzi warunkowych .Instrukcja rozgałęzienia może spowodować, że komputer zacznie wykonywać inną sekwencję instrukcji, a tym samym odbiega od domyślnego zachowania wykonywania instrukcji w kolejności (tj. Jeśli wyrażenie jest fałszywe, program pomija kod
if
bloku) w zależności od pewnego warunku, którym jest ocena wyrażenia w naszym przypadku.To powiedziawszy, kompilator próbuje przewidzieć wynik przed jego faktyczną oceną. Pobierze instrukcje z
if
bloku, a jeśli wyrażenie okaże się prawdziwe, to cudownie! Zyskaliśmy czas, aby go ocenić i poczynić postępy w kodzie; jeśli nie, to uruchamiamy zły kod, rurociąg jest opróżniany i uruchamiany jest poprawny blok.Wyobrażanie sobie:
Powiedzmy, że musisz wybrać trasę 1 lub trasę 2. Oczekiwanie na partnera, aby sprawdził mapę, zatrzymałeś się na ## i czekałeś, lub możesz po prostu wybrać trasę 1 i jeśli masz szczęście (trasa 1 jest prawidłową trasą), świetnie, że nie musiałeś czekać na sprawdzenie mapy przez partnera (zaoszczędziłeś czas, który zajęłoby mu sprawdzenie mapy), w przeciwnym razie po prostu zawrócisz.
Podczas gdy przepłukiwanie rurociągów jest super szybkie, w dzisiejszych czasach warto podjąć ten hazard. Przewidywanie posortowanych danych lub danych, które zmieniają się powoli, jest zawsze łatwiejsze i lepsze niż przewidywanie szybkich zmian.
źródło
W przypadku ARM nie jest wymagana gałąź, ponieważ każda instrukcja ma 4-bitowe pole warunku, które testuje (przy zerowym koszcie) dowolny z 16 różnych warunków, które mogą wystąpić w rejestrze statusu procesora, a jeśli warunek instrukcji jest false, instrukcja jest pomijana. Eliminuje to potrzebę krótkich gałęzi i nie byłoby trafienia prognozy gałęzi dla tego algorytmu. Dlatego posortowana wersja tego algorytmu działałaby wolniej niż nieposortowana wersja na ARM, z powodu dodatkowego obciążenia związanego z sortowaniem.
Wewnętrzna pętla dla tego algorytmu wyglądałaby mniej więcej tak jak w języku asemblera ARM:
Ale tak naprawdę jest to część większego obrazu:
CMP
opcodes zawsze aktualizują bity stanu w rejestrze stanu procesora (PSR), ponieważ taki jest ich cel, ale większość innych instrukcji nie dotyka PSR, chyba że dodasz opcjonalnyS
sufiks do instrukcji, określając, że PSR powinien być aktualizowany na podstawie wynik instrukcji. Podobnie jak 4-bitowy sufiks warunku, możliwość wykonywania instrukcji bez wpływu na PSR jest mechanizmem, który zmniejsza potrzebę rozgałęzień ARM, a także ułatwia wysyłanie poza kolejnością na poziomie sprzętowym , ponieważ po wykonaniu niektórych operacji X aktualizuje bity statusu, następnie (lub równolegle) możesz wykonać szereg innych prac, które wyraźnie nie powinny wpływać na bity statusu, a następnie możesz przetestować stan bitów statusu ustawiony wcześniej przez X.Pole testowania warunków i opcjonalne pole „ustaw bit stanu” można połączyć, na przykład:
ADD R1, R2, R3
wykonujeR1 = R2 + R3
bez aktualizacji bitów statusu.ADDGE R1, R2, R3
wykonuje tę samą operację tylko wtedy, gdy poprzednia instrukcja, która wpłynęła na bity statusu, spowodowała warunek Większy lub Równy.ADDS R1, R2, R3
Wykonuje dodawanie i aktualizujeN
,Z
,C
orazV
flagi w stanie procesora Rejestru podstawie tego, czy wynik był ujemny, zerowy, Przygotowane (dla unsigned dodatkowo) lub przepełnienie (dla podpisana dodatkowo).ADDSGE R1, R2, R3
wykonuje dodawanie tylko wtedy, gdyGE
test jest prawdziwy, a następnie aktualizuje bity statusu na podstawie wyniku dodawania.Większość architektur procesorów nie ma tej możliwości określania, czy bity statusu powinny być aktualizowane dla danej operacji, co może wymagać napisania dodatkowego kodu w celu zapisania i późniejszego przywrócenia bitów statusu, lub może wymagać dodatkowych rozgałęzień, lub może ograniczyć wydajność procesora wydajności wykonywania zleceń: jednym z efektów ubocznych większości architektur zestawów instrukcji CPU wymuszających aktualizację bitów statusu po większości instrukcji jest to, że znacznie trudniej jest rozdzielić, które instrukcje mogą być uruchamiane równolegle, nie zakłócając się nawzajem. Aktualizacja bitów stanu ma skutki uboczne, a zatem ma wpływ na linearyzację kodu.Zdolność ARM do mieszania i dopasowywania testów stanu bez rozgałęzień dla dowolnej instrukcji z opcją aktualizacji lub nie aktualizowania bitów statusu po każdej instrukcji jest niezwykle potężna, zarówno dla programistów i kompilatorów w języku asemblera, jak i wytwarza bardzo wydajny kod.
Jeśli kiedykolwiek zastanawiałeś się, dlaczego ARM odniósł tak fenomenalny sukces, genialna skuteczność i współdziałanie tych dwóch mechanizmów stanowią dużą część historii, ponieważ są jednym z największych źródeł wydajności architektury ARM. Blasku oryginalnych projektantów ARM ISA z 1983 roku, Steve'a Furbera i Rogera (obecnie Sophie) Wilsona, nie można przecenić.
źródło
R2 = data + arraySize
, a następnie zacznij odR1 = -arraySize
. Dolna część pętli staje sięadds r1, r1, #1
/bnz inner_loop
. Kompilatory nie używają tej optymalizacji z jakiegoś powodu: / Ale w każdym razie przewidywane wykonanie dodawania nie różni się zasadniczo w tym przypadku od tego, co można zrobić z kodem bez rozgałęzień na innych ISA, takich jak x86cmov
. Chociaż nie jest tak przyjemne: flaga optymalizacji gcc -O3 powoduje, że kod jest wolniejszy niż -O2cmov
zepsuły, w przeciwieństwie do x86 z operandem źródła pamięci. Większość ISA, w tym AArch64, ma tylko operacje wyboru ALU. Więc predykcja ARM może być potężna, i użyteczny bardziej efektywnie niż kod bez rozgałęzień na większości ISA.)Chodzi o przewidywanie gałęzi. Co to jest?
Predyktor gałęzi jest jedną ze starożytnych technik poprawiających wydajność, która wciąż znajduje zastosowanie w nowoczesnych architekturach. Podczas gdy proste techniki prognozowania zapewniają szybkie wyszukiwanie i efektywność energetyczną, cierpią z powodu wysokiego wskaźnika nieprzewidywalności.
Z drugiej strony, złożone przewidywania gałęzi - albo neuronowe, albo warianty dwupoziomowego przewidywania gałęzi - zapewniają lepszą dokładność przewidywania, ale zużywają więcej mocy, a złożoność rośnie wykładniczo.
Ponadto w przypadku złożonych technik przewidywania czas przewidziany na rozgałęzienia sam w sobie jest bardzo wysoki - od 2 do 5 cykli - co jest porównywalne z czasem wykonania rzeczywistych rozgałęzień.
Prognozowanie rozgałęzień jest zasadniczo problemem optymalizacji (minimalizacji), w którym nacisk kładziony jest na osiągnięcie najniższego możliwego wskaźnika pominięć, niskiego zużycia energii i niskiej złożoności przy minimalnych zasobach.
Naprawdę istnieją trzy różne rodzaje gałęzi:
Przekazywanie gałęzi warunkowych - w zależności od warunku działania komputer PC (licznik programu) jest zmieniany tak, aby wskazywał adres w strumieniu instrukcji.
Gałęzie warunkowe do tyłu - komputer jest zmieniany tak, aby wskazywał wstecz w strumieniu instrukcji. Rozgałęzienie opiera się na pewnych warunkach, takich jak rozgałęzienie wstecz do początku pętli programu, gdy test na końcu pętli stwierdza, że pętla powinna zostać wykonana ponownie.
Bezwarunkowe gałęzie - obejmuje to skoki, wywołania procedur i powroty, które nie mają określonego warunku. Na przykład bezwarunkowa instrukcja skoku może zostać zakodowana w języku asemblera jako po prostu „jmp”, a strumień instrukcji musi natychmiast zostać skierowany do miejsca docelowego wskazanego przez instrukcję skoku, podczas gdy skok warunkowy może być zakodowany jako „jmpne” przekieruje strumień instrukcji tylko wtedy, gdy wynik porównania dwóch wartości z poprzednich instrukcji „porównaj” wykaże, że wartości nie są równe. (Schemat adresowania segmentowego stosowany w architekturze x86 zwiększa złożoność, ponieważ skoki mogą być „bliskie” (w obrębie segmentu) lub „dalekie” (poza segmentem). Każdy typ ma inny wpływ na algorytmy przewidywania gałęzi.)
Przewidywanie rozgałęzień statycznych / dynamicznych : mikroprocesor stosuje przewidywanie rozgałęzień statycznych przy pierwszym napotkaniu rozgałęzienia warunkowego, a przewidywanie rozgałęzienia dynamicznego jest wykorzystywane do następnej realizacji kodu gałęzi warunkowej.
Bibliografia:
Predyktor gałęzi
Demonstracja samoprofilu
Przegląd prognoz branżowych
Prognozy branżowe
źródło
Oprócz tego, że przewidywanie gałęzi może cię spowolnić, posortowana tablica ma jeszcze jedną zaletę:
Możesz mieć warunek zatrzymania zamiast tylko sprawdzania wartości, w ten sposób zapętlasz tylko odpowiednie dane i ignorujesz resztę.
Prognozy dotyczące gałęzi zostaną pominięte tylko raz.
źródło
Posortowane tablice są przetwarzane szybciej niż nieposortowana tablica, ze względu na zjawisko zwane prognozowaniem gałęzi.
Predyktor rozgałęzienia to obwód cyfrowy (w architekturze komputerowej), który próbuje przewidzieć, w którą stronę pójdzie rozgałęzienie, poprawiając przepływ w potoku instrukcji. Obwód / komputer przewiduje następny krok i wykonuje go.
Dokonanie błędnej prognozy prowadzi do powrotu do poprzedniego kroku i wykonania z inną prognozą. Zakładając, że prognoza jest poprawna, kod przejdzie do następnego kroku. Niepoprawne przewidywanie powoduje powtarzanie tego samego kroku, dopóki nie nastąpi prawidłowe przewidywanie.
Odpowiedź na twoje pytanie jest bardzo prosta.
W nieposortowanej tablicy komputer dokonuje wielu prognoz, co prowadzi do zwiększonej szansy na błędy. Natomiast w posortowanej tablicy komputer dokonuje mniej prognoz, zmniejszając ryzyko błędów. Więcej prognoz wymaga więcej czasu.
Sorted Array: Straight Road ____________________________________________________________________________________ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - TTTTTTTTTTTTTTTTTTTTTTTTTTTT
Unsorted Array: Curved Road
Przewidywanie rozgałęzień: Zgadywanie / przewidywanie, która droga jest prosta i podążanie nią bez sprawdzania
Chociaż obie drogi docierają do tego samego celu, prosta droga jest krótsza, a druga dłuższa. Jeśli następnie przez pomyłkę wybierzesz inną, nie będzie już zawracania, a więc wybierzesz dłuższą drogę. Jest to podobne do tego, co dzieje się na komputerze i mam nadzieję, że pomogło ci to lepiej zrozumieć.
Chcę też zacytować @Simon_Weaver z komentarzy:
źródło
Próbowałem tego samego kodu z MATLAB 2011b z moim MacBookiem Pro (Intel i7, 64-bitowy, 2,4 GHz) dla następującego kodu MATLAB:
Wyniki dla powyższego kodu MATLAB są następujące:
Wyniki kodu C jak w @GManNickG otrzymuję:
Na tej podstawie wygląda, że MATLAB jest prawie 175 razy wolniejszy niż implementacja C bez sortowania i 350 razy wolniej z sortowaniem. Innymi słowy, efekt (przewidywania gałęzi) wynosi 1,46x dla implementacji MATLAB i 2,7x dla implementacji C.
źródło
Założenie innych odpowiedzi, że należy posortować dane, jest nieprawidłowe.
Poniższy kod nie sortuje całej tablicy, a jedynie 200-elementowe segmenty, dzięki czemu działa najszybciej.
Sortowanie tylko sekcji k-elementowych kończy przetwarzanie wstępne w czasie liniowym
O(n)
, a nieO(n.log(n))
czas potrzebny na sortowanie całej tablicy.To również „dowodzi”, że nie ma to nic wspólnego z jakimkolwiek zagadnieniem algorytmicznym, takim jak kolejność sortowania, i rzeczywiście jest to przewidywanie gałęzi.
źródło
pcmpgtb
aby znaleźć elementy z ich wysokim zestawem bitów, a następnie ORAZ, aby wyzerować mniejsze elementy). Spędzanie czasu na sortowaniu kawałków byłoby wolniejsze. Wersja bez rozgałęzienia miałaby wydajność niezależną od danych, co również dowodzi, że koszty wynikały z nieprzewidzianych oddziałów. Czy tylko liczniki wydajności użycie obserwować bezpośrednio, jak Skylakeint_misc.clear_resteer_cycles
lubint_misc.recovery_cycles
liczyć cykle jałowe front-end z mispredictsOdpowiedź Bjarne'a Stroustrupa na to pytanie:
To brzmi jak pytanie do wywiadu. Czy to prawda? Skąd mógłbyś wiedzieć? Odpowiadanie na pytania dotyczące wydajności bez uprzedniego wykonania niektórych pomiarów jest złym pomysłem, dlatego ważne jest, aby wiedzieć, jak mierzyć.
Próbowałem więc z wektorem miliona liczb całkowitych i otrzymałem:
Sprawdziłem to kilka razy, aby się upewnić. Tak, zjawisko jest prawdziwe. Mój kod klucza to:
Przynajmniej ten fenomen jest prawdziwy w przypadku tego kompilatora, biblioteki standardowej i ustawień optymalizatora. Różne implementacje mogą i dają różne odpowiedzi. W rzeczywistości ktoś przeprowadził bardziej systematyczne badanie (znajdzie je szybkie wyszukiwanie w Internecie) i większość implementacji wykazuje ten efekt.
Jednym z powodów jest przewidywanie gałęzi: kluczowa operacja w algorytmie sortowania jest
“if(v[i] < pivot]) …”
równoważna. W przypadku posortowanej sekwencji ten test jest zawsze prawdziwy, natomiast w przypadku sekwencji losowej wybrana gałąź zmienia się losowo.Innym powodem jest to, że kiedy wektor jest już posortowany, nigdy nie musimy przesuwać elementów do ich prawidłowej pozycji. Efekt tych drobnych szczegółów to współczynnik pięciu lub sześciu, które widzieliśmy.
Quicksort (i sortowanie ogólnie) to złożone badanie, które przyciągnęło jedne z największych umysłów informatyki. Dobra funkcja sortowania jest wynikiem zarówno wyboru dobrego algorytmu, jak i zwrócenia uwagi na wydajność sprzętu w jego implementacji.
Jeśli chcesz napisać wydajny kod, musisz wiedzieć trochę o architekturze maszyny.
źródło
To pytanie jest zakorzenione w modelach przewidywania rozgałęzień na procesorach. Polecam przeczytać ten artykuł:
Zwiększanie szybkości pobierania instrukcji poprzez przewidywanie wielu oddziałów i pamięć podręczną adresów oddziałów
Kiedy masz posortowane elementy, IR nie może mieć problemu z pobraniem wszystkich instrukcji procesora, raz po raz, pobiera je z pamięci podręcznej.
źródło
Jednym ze sposobów uniknięcia błędów prognozowania gałęzi jest zbudowanie tabeli odnośników i zindeksowanie jej przy użyciu danych. Stefan de Bruijn omówił to w swojej odpowiedzi.
Ale w tym przypadku wiemy, że wartości mieszczą się w zakresie [0, 255] i dbamy tylko o wartości> = 128. Oznacza to, że możemy łatwo wyodrębnić pojedynczy bit, który powie nam, czy chcemy wartość, czy nie: poprzez przesunięcie dane w prawych 7 bitach, mamy 0 bitów lub 1 bitów i chcemy dodać wartość tylko wtedy, gdy mamy 1 bit. Nazwijmy ten bit „bitem decyzyjnym”.
Używając wartości 0/1 bitu decyzyjnego jako indeksu w tablicy, możemy stworzyć kod, który będzie równie szybki, niezależnie od tego, czy dane zostaną posortowane, czy nie. Nasz kod zawsze doda wartość, ale gdy bit decyzyjny ma wartość 0, dodamy wartość w miejscu, w którym nas nie obchodzi. Oto kod:
// Test
Ten kod marnuje połowę wartości dodanych, ale nigdy nie występuje błąd przewidywania gałęzi. Jest losowo szybszy w przypadku danych losowych niż wersja z rzeczywistą instrukcją if.
Ale w moich testach jawna tabela odnośników była nieco szybsza niż ta, prawdopodobnie dlatego, że indeksowanie do tabeli odnośników było nieco szybsze niż przesuwanie bitów. To pokazuje, jak mój kod konfiguruje i korzysta z tabeli odnośników (niewyobrażalnie nazwanej lut dla „LookUp Table” w kodzie). Oto kod C ++:
// Zadeklaruj, a następnie wypełnij tabelę odnośników
W tym przypadku tablica przeglądowa miała tylko 256 bajtów, więc ładnie mieści się w pamięci podręcznej i wszystko było szybkie. Ta technika nie działałaby dobrze, gdyby dane były 24-bitowymi wartościami, a chcieliśmy tylko połowy z nich ... tabela przeglądowa byłaby o wiele za duża, aby była praktyczna. Z drugiej strony możemy połączyć dwie techniki pokazane powyżej: najpierw przesuń bity, a następnie zindeksuj tabelę wyszukiwania. W przypadku 24-bitowej wartości, której potrzebujemy tylko górnej połowy, możemy potencjalnie przesunąć dane w prawo o 12 bitów i pozostawić 12-bitową wartość dla indeksu tabeli. 12-bitowy indeks tabeli implikuje tabelę 4096 wartości, co może być praktyczne.
Technika indeksowania do tablicy zamiast użycia instrukcji if może być użyta do podjęcia decyzji, którego wskaźnika użyć. Widziałem bibliotekę, która zaimplementowała drzewa binarne i zamiast dwóch nazwanych wskaźników (pLeft i pRight lub cokolwiek innego) miała tablicę wskaźników o długości 2 i zastosowała technikę „bitu decyzyjnego”, aby zdecydować, który wybrać. Na przykład zamiast:
to dobre rozwiązanie, może zadziała
źródło
mask = tmp < 128 : 0 : -1UL;
/total += tmp & mask;