W szczególności, jeśli mam serię if
... else if
instrukcji i w jakiś sposób wiem z góry względne prawdopodobieństwo, że każde z nich oceni true
, na ile różni się czas wykonania, aby je posortować według prawdopodobieństwa? Na przykład, czy wolę to:
if (highly_likely)
//do something
else if (somewhat_likely)
//do something
else if (unlikely)
//do something
do tego?:
if (unlikely)
//do something
else if (somewhat_likely)
//do something
else if (highly_likely)
//do something
Wydaje się oczywiste, że posortowana wersja byłaby szybsza, jednak ze względu na czytelność lub istnienie efektów ubocznych możemy chcieć zamówić je nieoptymalnie. Trudno też powiedzieć, jak dobrze CPU poradzi sobie z przewidywaniem gałęzi, dopóki nie uruchomisz kodu.
Tak więc, w trakcie eksperymentowania z tym, ostatecznie odpowiedziałem na własne pytanie dotyczące konkretnego przypadku, ale chciałbym również usłyszeć inne opinie / spostrzeżenia.
Ważne: w tym pytaniu założono, że if
można dowolnie zmienić kolejność instrukcji, nie wywierając żadnego innego wpływu na zachowanie programu. W mojej odpowiedzi trzy testy warunkowe wykluczają się wzajemnie i nie powodują żadnych skutków ubocznych. Z pewnością, jeśli stwierdzenia muszą być ocenione w określonej kolejności, aby osiągnąć pożądane zachowanie, kwestia wydajności jest dyskusyjna.
Odpowiedzi:
Zasadniczo większość, jeśli nie wszystkie procesory Intel zakładają, że gałęzie do przodu nie są brane za pierwszym razem, gdy je zobaczą. Zobacz pracę Godbolta .
Następnie gałąź przechodzi do pamięci podręcznej prognoz gałęzi, a przeszłe zachowanie służy do informowania o prognozie przyszłych gałęzi.
Tak więc w ciasnej pętli efekt nieprawidłowego uporządkowania będzie stosunkowo niewielki. Predyktor gałęzi dowie się, który zestaw rozgałęzień jest najprawdopodobniej, a jeśli masz nie trywialną ilość pracy w pętli, małe różnice nie będą się sumować.
W ogólnym kodzie większość kompilatorów domyślnie (bez innego powodu) zamawia wygenerowany kod maszynowy mniej więcej tak, jak zamówiono go w kodzie. Zatem jeśli instrukcje są gałęziami forward, gdy zawiodą.
Powinieneś więc uporządkować swoje gałęzie w kolejności malejącego prawdopodobieństwa uzyskania najlepszej prognozy gałęzi po „pierwszym spotkaniu”.
Znak mikrobenchowania, który wielokrotnie zapętla się ściśle w szeregu warunków i wykonuje trywialną pracę, będzie zdominowany przez drobne efekty liczenia instrukcji i tym podobne, a także w niewielkim stopniu pod względem względnych problemów z przewidywaniem gałęzi. W takim przypadku musisz się profilować , ponieważ podstawowe zasady nie będą wiarygodne.
Ponadto wektoryzacja i wiele innych optymalizacji mają zastosowanie do małych ciasnych pętli.
Tak więc w ogólnym kodzie umieść najbardziej prawdopodobny kod w
if
bloku, a to spowoduje najmniejszą liczbę braków w prognozowaniu gałęzi bez pamięci podręcznej. W ciasnych pętlach postępuj zgodnie z ogólną zasadą, a jeśli chcesz dowiedzieć się więcej, nie masz innego wyboru, jak tylko profilować.Oczywiście wszystko to wychodzi poza okno, jeśli niektóre testy są znacznie tańsze niż inne.
źródło
Wykonałem następujący test, aby wykonać na czas wykonanie dwóch różnych
if
...else if
bloków, jeden posortowany według prawdopodobieństwa, a drugi posortowany w odwrotnej kolejności:Używając MSVC2017 z / O2, wyniki pokazują, że posortowana wersja jest konsekwentnie o około 28% szybsza niż wersja nieposortowana. Według komentarza luk32 zmieniłem również kolejność dwóch testów, co robi zauważalną różnicę (22% vs 28%). Kod został uruchomiony w systemie Windows 7 na procesorze Intel Xeon E5-2697 v2. Jest to oczywiście bardzo specyficzne dla problemu i nie powinno być interpretowane jako rozstrzygająca odpowiedź.
źródło
if... else if
instrukcji może mieć znaczący wpływ na przepływ logiki przez kod.unlikely
Czek nie może wymyślić często, ale może istnieć potrzeba biznesowych w celu sprawdzeniaunlikely
stanu najpierw przed sprawdzeniem innych.g++ -O2 -march=native -std=c++14
daje niewielką przewagę posortowanym instrukcjom warunkowym, ale przez większość czasu różnica procentowa między tymi dwoma przebiegami wynosiła ~ 5%. Kilka razy było tak naprawdę wolniej (ze względu na wariancje). Jestem całkiem pewien, że zamówienieif
takich produktów nie jest warte zmartwień; PGO prawdopodobnie całkowicie zajmie się takimi przypadkamiNie, nie powinieneś, chyba że naprawdę masz pewność, że dotyczy to systemu docelowego. Domyślnie przejść przez czytelność.
Bardzo wątpię w twoje wyniki. Trochę zmodyfikowałem twój przykład, aby łatwiej było wykonać cofanie. Ideone konsekwentnie pokazuje, że odwrotna kolejność jest szybsza, choć niewiele. Na niektórych biegach nawet to czasami się przewracało. Powiedziałbym, że wyniki nie są jednoznaczne. coliru nie zgłasza też żadnej różnicy. Mogę później sprawdzić procesor Exynos5422 na moim odroidu xu4.
Chodzi o to, że współczesne procesory mają predyktory gałęzi. Jest dużo logiki poświęconej pobieraniu zarówno danych, jak i instrukcji, a nowoczesne procesory x86 są dość inteligentne, jeśli chodzi o to. Niektóre cieńsze architektury, takie jak ARM lub GPU, mogą być na to podatne. Ale to naprawdę bardzo zależy zarówno od kompilatora, jak i systemu docelowego.
Powiedziałbym, że optymalizacja kolejności gałęzi jest dość delikatna i efemeryczna. Zrób to tylko jako naprawdę dopracowany krok.
Kod:
źródło
Tylko moje 5 centów. Wydaje się, że efekt zamówienia, jeśli instrukcje powinny zależeć od:
Prawdopodobieństwo każdej instrukcji if.
Liczba iteracji, aby predyktor gałęzi mógł się uruchomić.
Prawdopodobne / mało prawdopodobne wskazówki kompilatora, tj. Układ kodu.
Aby zbadać te czynniki, porównałem następujące funkcje:
order_ifs ()
Reverse_ifs ()
order_ifs_with_hints ()
reverse_ifs_with_hints ()
dane
Tablica danych zawiera liczby losowe od 0 do 100:
Wyniki
Poniższe wyniki dotyczą procesorów Intel i5 @ 3,2 GHz i G ++ 6.3.0. Pierwszy argument to punkt kontrolny (tzn. Prawdopodobieństwo w %% dla wysoce prawdopodobnej instrukcji if), drugi argument to data_sz (tj. Liczba iteracji).
Analiza
1. Zamówienie ma znaczenie
W przypadku iteracji 4K i (prawie) 100% prawdopodobieństwa bardzo podobającego się stwierdzenia różnica jest ogromna 223%:
W przypadku iteracji 4K i 50% prawdopodobieństwa bardzo podobającego się stwierdzenia różnica wynosi około 14%:
2. Liczba iteracji ma znaczenie
Różnica między iteracjami 4K i 8K dla (prawie) 100% prawdopodobieństwa bardzo lubianej wypowiedzi jest około dwa razy (zgodnie z oczekiwaniami):
Ale różnica między iteracjami 4K i 8K dla 50% prawdopodobieństwa bardzo lubianego zdania jest 5,5 razy:
Dlaczego tak jest Z powodu brakujących predyktorów gałęzi. Oto brakujące rozgałęzienia dla każdego wymienionego wyżej przypadku:
Tak więc na moim i5 predyktor gałęzi zawodzi spektakularnie w przypadku mało prawdopodobnych gałęzi i dużych zestawów danych.
3. Wskazówki Pomóż trochę
W przypadku iteracji 4K wyniki są nieco gorsze dla prawdopodobieństwa 50% i nieco lepsze dla prawdopodobieństwa bliskiego 100%:
Ale w przypadku iteracji 8K wyniki są zawsze nieco lepsze:
Wskazówki też pomagają, ale tylko trochę.
Ogólny wniosek jest następujący: zawsze testuj kod, ponieważ wyniki mogą zaskoczyć.
Mam nadzieję, że to pomaga.
źródło
g++ -O2
lub-O3 -fno-tree-vectorize
, ale powinieneś to powiedzieć.Na podstawie niektórych innych odpowiedzi tutaj wydaje się, że jedyną prawdziwą odpowiedzią jest: to zależy . Zależy to przynajmniej od następujących (choć niekoniecznie w tej kolejności):
Jedynym sposobem, aby wiedzieć na pewno, jest przeprowadzenie testu porównawczego konkretnego przypadku, najlepiej w systemie identycznym (lub bardzo podobnym) do zamierzonego systemu, w którym kod w końcu zostanie uruchomiony. Jeśli ma on działać na zestawie różnych systemów z różnym sprzętem, systemem operacyjnym itp., Dobrym pomysłem jest przetestowanie wielu odmian, aby sprawdzić, który jest najlepszy. Dobrym pomysłem może być kompilacja kodu z jednym zamówieniem na jednym typie systemu, a drugim na innym typie systemu.
Moją osobistą zasadą (w większości przypadków, w przypadku braku testu porównawczego) jest zamawianie na podstawie:
źródło
Sposób, w jaki zwykle postrzegam to rozwiązanie w przypadku kodu o wysokiej wydajności, polega na utrzymywaniu kolejności, która jest najbardziej czytelna, ale zapewnia kompilatorowi wskazówki. Oto jeden przykład z jądra Linuksa :
Tutaj zakłada się, że kontrola dostępu przejdzie i nie zostanie zwrócony żaden błąd
res
. Próba zmiany kolejności któregokolwiek z tych klauzul if spowoduje po prostu zamieszanie w kodzie, ale makralikely()
iunlikely()
faktycznie pomagają w czytelności, wskazując, co jest normalnym przypadkiem i jaki jest wyjątek.Implementacja tych makr w systemie Linux wykorzystuje funkcje specyficzne dla GCC . Wygląda na to, że kompilator clang i Intel C obsługuje tę samą składnię, ale MSVC nie ma takiej funkcji .
źródło
likely()
iunlikely()
makra są zdefiniowane i zawierają pewne informacje na temat odpowiedniej funkcji kompilatora.else if
jeśli kompilator nie jest wystarczająco inteligentny, aby wiedzieć, że warunki wzajemnie się wykluczają.Zależy również od twojego kompilatora i platformy, dla której kompilujesz.
Teoretycznie najbardziej prawdopodobny warunek powinien sprawić, że kontrola skoczy jak najmniej.
Zazwyczaj najbardziej prawdopodobnym warunkiem powinien być pierwszy:
Najpopularniejsze asmy oparte są na gałęziach warunkowych, które podskakują, gdy warunek jest spełniony . Ten kod C zostanie prawdopodobnie przetłumaczony na taki pseudo asm:
Wynika to z tego, że skoki powodują, że procesor anuluje potok wykonania i zatrzymuje się, ponieważ zmienił się licznik programu (dla architektur obsługujących potoki, które są naprawdę powszechne). Potem chodzi o kompilator, który może, ale nie musi, zastosować wyrafinowane optymalizacje dotyczące statystycznie najprawdopodobniej warunku, aby uzyskać kontrolę, wykonać mniej skoków.
źródło
clang
faktycznie przyjęło inne podejście do :test2
itest3
ze względu na heurystykę, która wskazuje, że a< 0
lub== 0
test może być fałszywy, postanowił sklonować pozostałą część funkcji na obu ścieżkach, aby był w stanie dokonaćcondition == false
upadku przez ścieżkę. Jest to możliwe tylko dlatego, że pozostała część funkcji jest krótka: wtest4
dodałem jeszcze jedną operację i wróciłem do podejścia opisanego powyżej.jmp
nie są przydatne, więc przepustowość pobierania / dekodowania jest marnowana (2), nawet przy przewidywaniu nowoczesne duże rdzenie wykonują tylko jedno pobieranie na cykl, więc nakłada twardy limit 1 branej gałęzi / cyklu (OTOH nowoczesny Intel może wykonać 2 nie brane / cykl) (3) ) trudniej jest przewidywać rozgałęzienia zajmować się kolejnymi rozgałęzieniami, aw przypadku predyktorów szybkich i wolnych ...Postanowiłem ponownie uruchomić test na własnym komputerze, używając kodu Lik32. Musiałem to zmienić, ponieważ moje okna lub kompilator uważają, że wysoka rozdzielczość to 1ms
mingw32-g ++. exe -O3 -Wall -std = c ++ 11 -wyrażenia -g
GCC dokonało tej samej transformacji na obu oryginalnych kodach.
Zauważ, że testowane są tylko dwa pierwsze warunki, ponieważ trzeci musi zawsze być prawdziwy, GCC jest tutaj rodzajem Sherlocka.
Odwrócić
To niewiele nam mówi, poza tym, że ostatni przypadek nie wymaga przewidywania oddziału.
Teraz wypróbowałem wszystkie 6 kombinacji if, pierwsze 2 są oryginalną odwrotnością i posortowane. high is> = 95, low is <20, mid is 20-94 with 10000000 iterations.
Dlaczego więc kolejność jest wysoka, niska, med, a następnie szybsza (nieznacznie)
Ponieważ najbardziej nieprzewidywalny jest ostatni i dlatego nigdy nie jest uruchamiany przez predyktor gałęzi.
Więc oddziały zostaną przepowiedziane, zabrane, zabrane i pozostały przy
6% + (0,94 *) 20% nieprzewidywalnych.
„Posortowane”
Oddziały będą przewidywane z nie zabranych, nie zabranych i Sherlocka.
25% + (0,75 *) 24% nieprzewidywalnych
Dajemy 18-23% różnicy (zmierzona różnica ~ 9%), ale musimy obliczyć cykle zamiast błędnego przewidywania%.
Załóżmy, że 17 cykli za niepoprawną karę na moim procesorze Nehalem i że każda kontrola zajmuje 1 cykl na wydanie (instrukcje 4-5), a pętla zajmuje również jeden cykl. Zależności danych to liczniki i zmienne pętlowe, ale gdy błędne prognozy staną się na przeszkodzie, nie powinno to wpłynąć na czas.
W przypadku „odwrotności” otrzymujemy czasy (powinna to być formuła stosowana w architekturze komputerowej: podejście ilościowe IIRC).
i to samo dla „posortowane”
(8,26–7,24) / 8,26 = 13,8% vs. ~ 9% zmierzone (blisko zmierzonych!?!).
Zatem oczywiste z PO nie jest oczywiste.
Dzięki tym testom inne testy z bardziej skomplikowanym kodem lub większą liczbą zależności danych na pewno będą się różnić, dlatego zmierz swoją sprawę.
Zmiana kolejności testu zmieniła wyniki, ale mogło to wynikać z różnych wyrównań początku pętli, które najlepiej powinny być wyrównane 16 bajtów na wszystkich nowszych procesorach Intela, ale tak nie jest.
źródło
Ułóż je w dowolnej logicznej kolejności. Oczywiście gałąź może być wolniejsza, ale rozgałęzienie nie powinno stanowić większości pracy wykonywanej przez komputer.
Jeśli pracujesz nad częścią kodu krytyczną pod względem wydajności, z pewnością użyj logicznej kolejności, optymalizacji kierowanej profilem i innych technik, ale w przypadku kodu ogólnego uważam, że jest to raczej wybór stylistyczny.
źródło
i++
kiedy++i
miałbym to zrobić, ponieważ zdaję sobie sprawę, żei++
dla niektórych iteratorów trudno jest je zoptymalizować,++i
a różnica (dla mnie) nie ma znaczenia. Chodzi o uniknięcie pesymizacji; umieszczenie najbardziej prawdopodobnego bloku na pierwszym miejscu jako domyślnego nawyku nie spowoduje zauważalnego zmniejszenia czytelności (i może faktycznie pomóc!), a jednocześnie spowoduje, że kod będzie przyjazny dla przewidywania gałęzi (a tym samym zapewni jednolity niewielki wzrost wydajności, którego nie można odzyskać późniejsza mikrooptymalizacja)Jeśli znasz już względne prawdopodobieństwo instrukcji if-else, dla celów wydajności lepiej posortować sposób, ponieważ sprawdzi on tylko jeden warunek (prawdziwy).
W nieposortowany sposób kompilator niepotrzebnie sprawdzi wszystkie warunki i zajmie trochę czasu.
źródło