Jakie są dobre strategie poprawy wydajności szeregowej mojego kodu?


66

Pracuję w nauce obliczeniowej, w wyniku czego spędzam nietrywialną ilość czasu próbując zwiększyć naukową przepustowość wielu kodów, a także zrozumieć ich efektywność.

Załóżmy, że oceniłem wydajność w porównaniu do czytelności / możliwości ponownego użycia / konserwacji kompromisu oprogramowania, nad którym pracuję, i zdecydowałem, że nadszedł czas na wydajność. Załóżmy również, że wiem, że nie mam lepszego algorytmu dla mojego problemu (pod względem flop / si przepustowości pamięci). Możesz również założyć, że moja baza kodu jest w języku niskiego poziomu, takim jak C, C ++ lub Fortran. Na koniec załóżmy, że w kodzie nie ma paralelizmu lub że interesuje nas wydajność tylko na jednym rdzeniu.

Jakie są najważniejsze rzeczy do wypróbowania w pierwszej kolejności? Skąd mam wiedzieć, ile wydajności mogę uzyskać?

Odpowiedzi:


66

Po pierwsze, jak zauważyli specjaliści i Dan , profilowanie jest niezbędne. Osobiście używam wzmacniacza VTune Intela na Linuksie, ponieważ daje mi on bardzo szczegółowy przegląd tego, gdzie spędziłem czas, robiąc co.

Jeśli nie zamierzasz zmieniać algorytmu (tj. Jeśli nie będzie żadnych poważniejszych zmian, które spowodują, że wszystkie Twoje optymalizacje staną się przestarzałe), sugeruję poszukiwanie wspólnych typowych szczegółów implementacji, które mogą mieć duże znaczenie:

  • Miejsce w pamięci : czy dane, które są odczytywane / wykorzystywane razem, są również przechowywane razem, czy też zbierasz fragmenty tu i tam?

  • Wyrównanie pamięci : czy twoje kopie są wyrównane do 4 bajtów? Jak się spakowałeś structs? Aby być pedantycznym, użyj posix_memalignzamiast malloc.

  • Wydajność pamięci podręcznej : lokalizacja zajmuje się większością problemów z wydajnością pamięci podręcznej, ale jeśli masz małe małe struktury danych, które często odczytujesz / zapisujesz, pomaga to, jeśli są one całkowitą wielokrotnością lub ułamkiem linii pamięci podręcznej (zwykle 64 bajty). Pomaga również, jeśli dane są wyrównane do wielkości linii pamięci podręcznej. Może to drastycznie zmniejszyć liczbę odczytów niezbędnych do załadowania kawałka danych.

  • Wektoryzacja : Nie, nie idź z umysłem z ręcznie kodowanym asemblerem. gccoferuje typy wektorów, które są tłumaczone automatycznie na SSE / AltiVec / cokolwiek instrukcje automatycznie.

  • Równoległość na poziomie instrukcji : drański syn wektoryzacji. Jeśli niektóre często powtarzane obliczenia nie wektoryzują się dobrze, możesz spróbować zgromadzić wartości wejściowe i obliczyć kilka wartości jednocześnie. To trochę jak rozwijanie pętli. Wykorzystujesz tutaj to, że twój procesor zwykle ma więcej niż jedną jednostkę zmiennoprzecinkową na rdzeń.

  • Precyzja arytmetyczna : czy naprawdę potrzebujesz arytmetyki podwójnej precyzji we wszystkim, co robisz? Na przykład, jeśli obliczasz poprawkę w iteracji Newtona, zwykle nie potrzebujesz wszystkich cyfr, które obliczasz. Bardziej szczegółową dyskusję można znaleźć w tym dokumencie.

Niektóre z tych sztuczek są używane w daxpy_cvec tym wątku. To powiedziawszy, jeśli używasz Fortrana (nie jest to język niskiego poziomu w moich książkach), będziesz miał bardzo niewielką kontrolę nad większością tych „sztuczek”.

Jeśli korzystasz z jakiegoś dedykowanego sprzętu, np. Klastra, którego używasz do wszystkich uruchomień produkcyjnych, możesz także przeczytać o szczegółach używanych procesorów. Nie dlatego, że powinieneś pisać rzeczy w asemblerze bezpośrednio dla tej architektury, ale może zainspirować cię do znalezienia innych optymalizacji, które mogłeś przegapić. Znajomość funkcji jest niezbędnym pierwszym krokiem do napisania kodu, który może ją wykorzystać.

Aktualizacja

Minęło trochę czasu, odkąd to napisałem i nie zauważyłem, że stała się tak popularną odpowiedzią. Z tego powodu chciałbym dodać jedną ważną kwestię:

  • Porozmawiaj z lokalnym informatykiem : czy nie byłoby fajnie, gdyby istniała dyscyplina, która zajmowałaby się wyłącznie zwiększaniem wydajności algorytmów i / lub obliczeń, a my wszyscy moglibyśmy poprosić ich o radę? Dobra wiadomość, że ta dyscyplina istnieje: informatyka. Możliwe, że twoja instytucja ma nawet cały dział poświęcony temu. Porozmawiaj z tymi facetami.

Jestem pewien, że wielu nie-informatyków przywoła wspomnienia frustrujących dyskusji ze wspomnianą dyscypliną, które doprowadziły do ​​niczego, lub wspomnienia anegdot innych ludzi. Nie zniechęcaj się. Współpraca interdyscyplinarna jest trudna i wymaga trochę pracy, ale nagrody mogą być ogromne.

Z mojego doświadczenia wynika, że ​​jako informatyk (CS) sztuczka polega na tym, aby zarówno oczekiwania, jak i komunikacja były prawidłowe.

Oczekiwania - CS pomoże Ci tylko wtedy, gdy uzna, że ​​Twój problem jest interesujący. To prawie wyklucza próbę optymalizacji / wektoryzacji / równoległości fragmentu kodu, który napisałeś, ale tak naprawdę nie skomentowałeś, z powodu problemu, którego nie rozumieją. CS są zwykle bardziej zainteresowani podstawowym problemem, np. Algorytmami zastosowanymi do jego rozwiązania. Nie dawaj im swojego rozwiązania , daj im swój problem .

Przygotuj się także na to, że CS powie „ ten problem został już rozwiązany ” i po prostu podam odniesienie do artykułu. Porada: przeczytaj ten artykuł i, jeśli naprawdę dotyczy on twojego problemu, zastosuj dowolny algorytm, który on sugeruje. To nie jest CS, który jest zadowolony z siebie, to CS, który właśnie ci pomógł. Nie obrażaj się, pamiętaj: jeśli problem nie jest interesujący pod względem obliczeniowym, tj. Został już rozwiązany, a rozwiązanie okazało się optymalne, nie będą na nim działać, a tym bardziej kodowanie dla Ciebie.

Jeśli chodzi o komunikację , pamiętaj, że większość CS nie jest ekspertami w swojej dziedzinie i wyjaśnij problem pod względem tego , co robisz, a nie jak i dlaczego . Zazwyczaj naprawdę nie dbamy o to, dlaczego i jak to robimy najlepiej.

Na przykład obecnie pracuję z grupą kosmologów obliczeniowych nad napisaniem lepszej wersji ich kodu symulacyjnego, opartej na SPH i multipolach . Zajęło około trzech spotkań, aby przestać mówić w kategoriach ciemnej materii i halonów galaktyk (huh?) I przejść do sedna obliczeń, tj. Że muszą znaleźć wszystkich sąsiadów w danym promieniu każdej cząstki, obliczyć niektóre ilość nad nimi, a następnie ponownie przejrzyj wszystkich wymienionych sąsiadów i zastosuj tę ilość w innym obliczeniu. Następnie przenieś cząstki lub przynajmniej niektóre z nich i zrób to wszystko jeszcze raz. Widzisz, podczas gdy ten pierwszy może być niezwykle interesujący (tak jest!), Ten drugi jest tym, co muszę zrozumieć, aby zacząć myśleć o algorytmach.

Ale odchodzę od głównego punktu: jeśli naprawdę jesteś zainteresowany przyspieszeniem obliczeń, a sam nie jesteś informatykiem, porozmawiaj z jednym.


4
wraz z narzędziami do profilowania nie zapomnę o valgrind .
GertVdE

1
Zgadzam się z tobą Pedro, kiedy optymalizowany program jest jak samochód wyścigowy F1, już prawie optymalny. Programy, które widzę w praktyce, naukowe i nie, często przypominają Cadillac Coupe DeVilles. Aby uzyskać prawdziwą wydajność, można usunąć mnóstwo tłuszczu. Następnie golenie cykliczne zaczyna działać.
Mike Dunlavey,

1
@MikeDunlavey: Całkowicie się zgadzam. Dodałem aktualizację do mojej odpowiedzi, aby rozwiązać więcej problemów związanych z algorytmem.
Pedro

1
@MikeDunlavey, jestem CS folk :)
Pedro

2
Zademonstrowałem to w przemówieniu na U Mass. Lowell. To było demo na żywo, pokazujące wszystkie etapy przyspieszenia 730x. Myślę, że jeden profesor zrozumiał, z pół tuzina.
Mike Dunlavey,

38

Oprogramowanie naukowe nie różni się tak bardzo od innych programów, jak wiedzieć, co wymaga strojenia.

Metoda, której używam, to losowe wstrzymywanie . Oto niektóre z przyspieszeń, które dla mnie znalazł:

Jeśli duża część czasu jest poświęcana na funkcje takie jak logi exp, widzę, jakie są argumenty tych funkcji, w zależności od punktów, z których są wywoływane. Często są oni wywoływani wielokrotnie z tym samym argumentem. Jeśli tak, zapamiętywanie powoduje ogromny współczynnik przyspieszenia.

Jeśli korzystam z funkcji BLAS lub LAPACK, może się okazać, że duża część czasu poświęcana jest na procedury kopiowania tablic, mnożenia macierzy, transformacji choleski itp.

  • Rutynowe kopiowanie tablic nie jest dostępne dla szybkości, jest dla wygody. Może się okazać, że jest to mniej wygodny, ale szybszy sposób na zrobienie tego.

  • Procedury mnożenia lub odwracania macierzy lub wykonywania transformacji choleskiego zwykle zawierają argumenty znakowe określające opcje, takie jak „U” lub „L” dla trójkąta górnego lub dolnego. Ponownie są one dostępne dla wygody. Odkryłem, że ponieważ moje matryce nie były bardzo duże, procedury spędzały ponad połowę czasu na wzywaniu podprogramu do porównywania znaków w celu rozszyfrowania opcji. Pisanie specjalnych wersji najbardziej kosztownych procedur matematycznych spowodowało ogromne przyspieszenie.

Jeśli mogę po prostu rozwinąć to drugie: procedura mnożenia macierzy DGEMM wywołuje LSAME w celu zdekodowania argumentów znaków. Patrząc na uwzględniające procent czasu (jedyne statystyki, na które warto spojrzeć) profilery uważane za „dobre” mogą pokazywać DGEMM wykorzystujący pewien procent całkowitego czasu, np. 80%, a LSAME wykorzystujący pewien procent całkowitego czasu, np. 50%. Patrząc na te pierwsze, kusiłoby cię, by powiedzieć „dobrze, to musi być mocno zoptymalizowane, więc niewiele mogę na to poradzić”. Patrząc na to drugie, kusiłaby cię odpowiedź: „Hę? O co w tym wszystkim chodzi? To tylko drobna rutyna. Ten profiler musi się mylić!”

To nie jest źle, po prostu nie mówi ci, co musisz wiedzieć. Losowe wstrzymywanie pokazuje, że DGEMM znajduje się na 80% próbek stosu, a LSAME na 50%. (Nie potrzeba dużo próbek, aby to wykryć. 10 to zwykle dużo.) Co więcej, w wielu z tych próbek DGEMM właśnie wywołuje LSAME z kilku różnych linii kodu.

Teraz już wiesz, dlaczego obie procedury zabierają tyle czasu. Wiesz także, skąd w kodzie są wywoływani, aby spędzać cały ten czas. Dlatego używam losowego pauzowania i przeglądam profilowane profile bez względu na to, jak są dobrze wykonane. Bardziej interesują ich pomiary niż informowanie o tym, co się dzieje.

Łatwo jest założyć, że procedury biblioteczne matematyki zostały zoptymalizowane do n-tego stopnia, ale w rzeczywistości zostały zoptymalizowane tak, aby były przydatne do szerokiego zakresu celów. Musisz zobaczyć, co się naprawdę dzieje, a nie to, co łatwo założyć.

DODANO: Aby odpowiedzieć na dwa ostatnie pytania:

Jakie są najważniejsze rzeczy do wypróbowania w pierwszej kolejności?

Pobierz 10–20 próbek stosu i nie podsumowuj ich, zrozum, co każdy z nich mówi. Zrób to pierwszy, ostatni i pomiędzy. (Nie ma „próby”, młody Skywalker.)

Skąd mam wiedzieć, ile wydajności mogę uzyskać?

xβ(s+1,(ns)+1)sn1/(1x)n=10s=5x
wprowadź opis zdjęcia tutaj
xx

Jak już wcześniej wspomniałem, możesz powtarzać całą procedurę, aż nie będziesz już w stanie, a złożony współczynnik przyspieszenia może być dość duży.

(s+1)/(n+2)=3/22=13.6%.) Dolna krzywa na poniższym wykresie to jej rozkład:

wprowadź opis zdjęcia tutaj

Zastanów się, czy pobraliśmy aż 40 próbek (więcej niż kiedykolwiek naraz) i zauważyliśmy problem tylko na dwóch z nich. Szacunkowy koszt (tryb) tego problemu wynosi 5%, jak pokazano na wyższej krzywej.

Co to jest „fałszywie pozytywny”? Chodzi o to, że jeśli naprawisz problem, zdajesz sobie sprawę, że zyskujesz tak mniej niż oczekiwano, że żałujesz, że go naprawiłeś. Krzywe pokazują (jeśli problem jest „mały”), że chociaż wzmocnienie może być mniejsze niż ułamek próbek, które go pokazują, to średnio będzie ono większe.

Istnieje znacznie poważniejsze ryzyko - „fałszywie ujemny”. Wtedy pojawia się problem, ale go nie znaleziono. (Przyczynia się to do „uprzedzenia potwierdzającego”, w którym brak dowodów jest zwykle traktowany jako dowód braku).

Za pomocą profilera (dobrego) otrzymujesz znacznie dokładniejszy pomiar (a tym samym mniejsze prawdopodobieństwo fałszywych alarmów), kosztem znacznie mniej dokładnych informacji o tym, co w rzeczywistości jest problemem (a tym samym mniejszą szansą na jego znalezienie i uzyskanie wszelkie zyski). Ogranicza to ogólne przyspieszenie, które można osiągnąć.

Zachęcam użytkowników profilerów do zgłaszania czynników przyspieszenia, które faktycznie uzyskują w praktyce.


Jest jeszcze jedna kwestia do zrobienia. Pytanie Pedro dotyczące fałszywych trafień.

Wspomniał, że może istnieć trudność w przejściu do drobnych problemów w wysoce zoptymalizowanym kodzie. (Dla mnie mały problem to taki, który stanowi 5% lub mniej całkowitego czasu.)

Ponieważ jest całkowicie możliwe zbudowanie programu, który jest całkowicie optymalny, z wyjątkiem 5%, kwestię tę można rozwiązać tylko empirycznie, jak w tej odpowiedzi . Aby uogólnić na podstawie doświadczenia empirycznego, wygląda to tak:

Program, jak napisano, zazwyczaj zawiera kilka możliwości optymalizacji. (Możemy nazwać je „problemami”, ale często są one doskonale dobrym kodem, po prostu zdolnym do znacznej poprawy.) Ten schemat ilustruje sztuczny program zajmujący trochę czasu (powiedzmy 100s) i zawiera problemy A, B, C, ... że po znalezieniu i naprawieniu zaoszczędzisz 30%, 21% itd. oryginalnych setek.

wprowadź opis zdjęcia tutaj

Zauważ, że problem F kosztuje 5% pierwotnego czasu, więc jest „mały” i trudny do znalezienia bez 40 lub więcej próbek.

Jednak pierwsze 10 próbek z łatwością znajduje problem A. ** Gdy problem zostanie rozwiązany, program zajmuje tylko 70s, dla przyspieszenia 100/70 = 1,43x. To nie tylko przyspiesza działanie programu, ale także powiększa, o ten współczynnik, wartości procentowe pozostałych problemów. Na przykład problem B początkowo zajmował 21 s, co stanowiło 21% całości, ale po usunięciu A B bierze 21 z 70, czyli 30%, więc łatwiej jest znaleźć, gdy cały proces się powtarza.

Po pięciokrotnym powtórzeniu procesu czas wykonania wynosi 16,8 s, z czego problem F wynosi 30%, a nie 5%, więc łatwo jest znaleźć 10 próbek.

Więc o to chodzi. Empirycznie, programy zawierają szereg problemów mających rozkład rozmiarów, a każdy znaleziony i naprawiony problem ułatwia znalezienie pozostałych. Aby to osiągnąć, żaden z problemów nie może zostać pominięty, ponieważ jeśli tak, siedzą tam, poświęcając czas, ograniczając całkowite przyspieszenie i nie powiększając pozostałych problemów. Dlatego bardzo ważne jest, aby znaleźć ukryte problemy .

Jeśli problemy od A do F zostaną znalezione i naprawione, przyspieszenie wynosi 100 / 11,8 = 8,5x. Jeśli jeden z nich zostanie pominięty, na przykład D, wówczas przyspieszenie wynosi tylko 100 / (11,8 + 10,3) = 4,5x. To cena zapłacona za fałszywe negatywy.

Tak więc, gdy profiler mówi „nie ma tutaj żadnego istotnego problemu” (tj. Dobry koder, jest to praktycznie optymalny kod), być może ma rację, a może nie. ( Fałszywie negatywny .) Nie wiesz na pewno, czy jest więcej problemów do naprawienia, w celu przyspieszenia, chyba że spróbujesz innej metody profilowania i odkryjesz, że są. Z mojego doświadczenia wynika, że ​​metoda profilowania nie wymaga dużej liczby próbek, w skrócie, ale niewielkiej liczby próbek, przy czym każda próbka jest wystarczająco dokładnie rozumiana, aby rozpoznać każdą możliwość optymalizacji.

2/0.3=6.671 - pbinom(1, numberOfSamples, sizeOfProblem)1 - pbinom(1, 20, 0.3) = 0.9923627

xβ(s+1,(ns)+1)nsy1/(1x)xyy1Dystrybucja BetaPrime . Symulowałem to z 2 milionami próbek, dochodząc do tego zachowania:

         distribution of speedup
               ratio y

 s, n    5%-ile  95%-ile  mean
 2, 2    1.58    59.30   32.36
 2, 3    1.33    10.25    4.00
 2, 4    1.23     5.28    2.50
 2, 5    1.18     3.69    2.00
 2,10    1.09     1.89    1.37
 2,20    1.04     1.37    1.17
 2,40    1.02     1.17    1.08

 3, 3    1.90    78.34   42.94
 3, 4    1.52    13.10    5.00
 3, 5    1.37     6.53    3.00
 3,10    1.16     2.29    1.57
 3,20    1.07     1.49    1.24
 3,40    1.04     1.22    1.11

 4, 4    2.22    98.02   52.36
 4, 5    1.72    15.95    6.00
 4,10    1.25     2.86    1.83
 4,20    1.11     1.62    1.31
 4,40    1.05     1.26    1.14

 5, 5    2.54   117.27   64.29
 5,10    1.37     3.69    2.20
 5,20    1.15     1.78    1.40
 5,40    1.07     1.31    1.17

(n+1)/(ns)s=ny

Jest to wykres rozkładu współczynników przyspieszenia i ich średnich dla 2 trafień z 5, 4, 3 i 2 próbek. Na przykład, jeśli zostaną pobrane 3 próbki, a 2 z nich to trafienie w problem, a problem ten można usunąć, średni współczynnik przyspieszenia wyniesie 4x. Jeśli 2 trafienia są widoczne tylko w 2 próbkach, średnie przyspieszenie jest niezdefiniowane - koncepcyjnie, ponieważ programy z nieskończonymi pętlami istnieją z niezerowym prawdopodobieństwem!

wprowadź opis zdjęcia tutaj


1
Uhm ... Czy nie otrzymujesz dokładnie tych informacji, patrząc na wykresy wywołań profilera lub podsumowania typu „oddolne” dostarczone przez VTune?
Pedro

2
@Pedro: Jeśli tylko. W próbce stosu (i powiązanych zmiennych) jest zakodowany cały powód, dla którego spędzany jest przyrost czasu. Nie możesz się go pozbyć, jeśli nie wiesz, dlaczego jest wydawany. Niektóre problemy można znaleźć przy ograniczonej ilości informacji, ale nie każdy . Jeśli dostaniesz tylko niektóre z nich, ale nie wszystkie, problemy, których nie dostaniesz, uniemożliwiają dalsze przyspieszenie. Sprawdź tutaj i tutaj .
Mike Dunlavey,

Prawdopodobnie porównujesz swoją metodę do złego profilowania ... Możesz również przejść przez profil dla każdej procedury, niezależnie od jej wkładu w całkowity czas wykonania, i poszukać ulepszeń z tym samym efektem. W swoim podejściu martwię się o rosnącą liczbę fałszywych alarmów, które będziesz śledzić, gdy „punkty aktywne” w kodzie będą coraz mniejsze.
Pedro

@Pedro: Po prostu pobieraj próbki, aż zobaczysz coś, co możesz naprawić na więcej niż jednej próbce. Dystrybucja beta mówi, ile możesz zaoszczędzić, jeśli cię to obchodzi, ale jeśli boisz się, że przyśpieszysz mniej, niż pokazuje, pamiętaj, że odrzucasz szansę, że może to być więcej (i jest wypaczone ). Większe niebezpieczeństwo, przy podsumowaniu profilerów, to fałszywe negatywy . Może być problem, ale masz tylko nadzieję, że twoja intuicja go wyczuje, gdy profiler nie jest konkretny w kwestii tego, gdzie może być.
Mike Dunlavey,

@Pedro: Jedyną słabością, jaką znam, jest to, że patrząc na migawkę, nie możesz dowiedzieć się, dlaczego ten czas jest spędzany, na przykład czy po prostu przetwarza asynchroniczne zdarzenia, w których ukrywa się requester, lub asynchroniczne protokoły. Aby uzyskać więcej „normalnego” kodu, pokaż mi „dobry” profiler, a pokażę ci problem, z którym ma problemy lub po prostu nie może go znaleźć (powodując, że cofasz się do swoich omylnych smartów). Zasadniczo sposobem na zbudowanie takiego problemu jest upewnienie się, że realizowany cel nie może być odszyfrowany lokalnie. I takie problemy obfitują w oprogramowanie.
Mike Dunlavey,

23

Nie tylko musisz mieć dogłębną wiedzę na temat swojego kompilatora , ale także masz wiedzę na temat architektury docelowej i systemu operacyjnego .

Co może wpłynąć na wydajność?

Jeśli chcesz wycisnąć każdą uncję wydajności, to za każdym razem, gdy zmieniasz docelową architekturę, będziesz musiał poprawić i ponownie zoptymalizować kod. Coś, co było optymalizacją z jednym procesorem, może stać się nieoptymalne w następnej wersji tego samego procesora.

Doskonałym tego przykładem są pamięci podręczne procesora. Przenieś swój program z procesora z szybką małą pamięcią podręczną na pamięć o nieco wolniejszej, nieco większej pamięci podręcznej, a profilowanie może się znacznie zmienić.

Nawet jeśli architektura docelowa się nie zmienia, zmiany niskiego poziomu w systemie operacyjnym mogą również wpływać na wydajność. Łatki Spectre i Meltdown miały ogromny wpływ na niektóre obciążenia, więc mogą wymusić ponowną ocenę twoich optymalizacji.

Jak mogę zoptymalizować kod?

Tworząc wysoce zoptymalizowany kod, musisz zachować jego modułowość i ułatwić zamianę różnych wersji tego samego algorytmu na wejścia i wyjścia, być może nawet wybierając konkretną wersję używaną w czasie wykonywania, w zależności od dostępnych zasobów i wielkości / złożoności dane do przetworzenia.

Modułowość oznacza również możliwość korzystania z tego samego zestawu testów we wszystkich zoptymalizowanych i niezoptymalizowanych wersjach, co pozwala zweryfikować, czy wszystkie zachowują się tak samo i szybko profilować każdą z nich w porównaniu podobnym do podobnego . W mojej odpowiedzi na pytanie Jak dokumentować i uczyć innych „zoptymalizowanych nie do poznania” intensywnie obliczeniowych kodów, wchodzę w nieco więcej szczegółów ? .

Dalsza lektura

Ponadto gorąco polecam przyjrzenie się doskonałemu artykułowi Ulricha Dreppera Co każdy programista powinien wiedzieć o pamięci , którego tytuł jest hołdem dla równie fantastycznego Davida Goldberga, co każdy informatyk powinien wiedzieć o arytmetyki zmiennoprzecinkowej .

Pamiętaj, że każda optymalizacja może stać się przyszłą antyoptymalizacją , dlatego należy ją uznać za możliwy zapach kodu, który należy ograniczyć do minimum. Moja odpowiedź na pytanie: Czy mikrooptymalizacja jest ważna podczas kodowania? stanowi konkretny przykład tego z własnego doświadczenia.


8

Myślę, że pytanie sformułujesz zbyt wąsko. Moim zdaniem, użytecznym podejściem jest żyć przy założeniu, że tylko zmiany w strukturach danych i algorytmach mogą przynieść znaczący wzrost wydajności kodów, które mają więcej niż kilka 100 linii, i uważam, że muszę jeszcze znaleźć kontrprzykład to roszczenie.


3
Zgadza się co do zasady, ale nie należy lekceważyć zależności między wydajnością algorytmu / struktury danych a szczegółami podstawowego sprzętu. Np. Zrównoważone drzewa binarne świetnie nadają się do wyszukiwania / przechowywania danych, ale w zależności od opóźnienia pamięci globalnej tablica skrótów może być lepsza.
Pedro

1
Zgoda. Algorytmy i struktura danych mogą zapewnić ulepszenia od O (10) do O (100). Jednak w przypadku kilku problemów związanych z obliczeniami (jak w przypadku obliczeń dynamiki molekularnej, astrofizyki, przetwarzania obrazu i wideo w czasie rzeczywistym, finansów) wysoce dostrojona pętla krytyczna może oznaczać od 3 do 10 razy szybszy ogólny czas działania aplikacji.
fcruz

Widziałem źle uporządkowane pętle zagnieżdżone w znacznych rozmiarach kodów „produkcyjnych”. Poza tym myślę, że masz rację.
dmckee,

8

Pierwszą rzeczą, którą powinieneś zrobić, to profilować swój kod. Chcesz dowiedzieć się, które części programu spowalniają cię, zanim zaczniesz optymalizować, w przeciwnym razie możesz skończyć optymalizowaniem części kodu, która i tak nie pochłaniała dużo czasu wykonywania.

Linux

gprof jest całkiem dobry, ale mówi tylko, ile czasu zajmuje każda funkcja, a nie każda linia.

Apple OS X

Możesz wypróbować Sharka . Jest on dostępny w witrynie Apple dla programistów pod Pobrane> Narzędzia dla programistów> CHUD 4.6.2, starsza wersja tutaj . CHUD zawiera również inne narzędzia do profilowania, takie jak nakładka BigTop, narzędzie do wyszukiwania indeksu PMC, profiler na poziomie funkcji Saturn i wiele innych poleceń. Shark będzie dostępny w wersji wiersza poleceń.


Profil +1? Tak, w pewnym sensie ... To znacznie lepsze niż zgadywanie, ale oto lista problemów, które dotyczą szczególnie gprof i wielu innych profilerów.
Mike Dunlavey,

Czy Shark to stare polecenie w systemie OS X? Więcej tutaj . Czy w Mountain Lion powinienem używać instrumentów?
hhh

@ hhh: To był profil GUI dla komputerów Mac, choć wygląda na to, że nie jest już utrzymywany. Nie programowałem na maszynie do jabłek, odkąd napisałem tę odpowiedź, więc nie mogę ci bardzo pomóc.
Dan

1
Jest dostępny w witrynie Apple dla programistów w sekcji Pliki do pobrania> Narzędzia dla programistów> CHUD 4.6.2. Starsza wersja tutaj i zawiera wszelkiego rodzaju profilowanie - niestety instalacja się nie udaje: „Skontaktuj się z producentem”, nie mam pojęcia o błędzie. Rekin został usunięty z Xcodea najwyraźniej po Lion, a później został ponownie umieszczony na stronie Apple Dev po tym, jak był darmowym narzędziem w MacUpdate.
hhh

@ hhh: Wydajesz się bardziej wykwalifikowany, by na to odpowiedzieć niż ja. Edytuj moją odpowiedź, aby ją zaktualizować, lub napisz własną.
Dan

7

Jeśli chodzi o wydajność, możesz uzyskać wyniki z profilowania kodu i powiedzmy, że identyfikujesz kawałek, który zajmuje ułamek czasu „p”. Jeśli miałbyś poprawić wydajność tego kawałka tylko o współczynnik „s”, twoje ogólne przyspieszenie wyniesie 1 / ((1-p) + p / s). Dlatego możesz maksymalnie zwiększyć prędkość o współczynnik 1 / (1-p). Mam nadzieję, że masz obszary o wysokim p! Jest to odpowiednik prawa Amdahla do optymalizacji szeregowej.


5

Optymalizacja kodu musi być wykonana ostrożnie. Załóżmy również, że już debugowałeś kod. Możesz zaoszczędzić dużo czasu, przestrzegając określonych priorytetów, a mianowicie:

  1. W miarę możliwości używaj wysoce zoptymalizowanych (lub profesjonalnie zoptymalizowanych) bibliotek. Niektóre przykłady mogą obejmować biblioteki FFTW, OpenBlas, Intel MKL, NAG itp. O ile nie jesteś utalentowany (jak programista GotoBLAS), prawdopodobnie nie będziesz w stanie pokonać profesjonalistów.

  2. Użyj profilera (sporo z poniższej listy zostało już nazwanych w tym wątku - Intel Tune, valgrind, gprof, gcov itp.), Aby dowiedzieć się, które części kodu zajmują najwięcej czasu. Nie ma sensu marnować czasu na optymalizację części kodu, które są rzadko wywoływane.

  3. Na podstawie wyników profilera spójrz na część kodu, która zajęła najwięcej czasu. Określ naturę algorytmu - czy jest on związany z procesorem, czy z pamięcią? Każda z nich wymaga innego zestawu technik optymalizacji. Jeśli często brakuje pamięci podręcznej, pamięć może być wąskim gardłem - procesor marnuje cykle zegara, czekając na dostępność pamięci. Zastanów się, czy pętla pasuje do pamięci podręcznej L1 / L2 / L3 twojego systemu. Jeśli masz w pętli instrukcje „if”, sprawdź, czy profiler mówi coś o nieprzewidywalności gałęzi? Jaka jest kara za nieprzewidziane oddziały twojego systemu? Nawiasem mówiąc, możesz uzyskać dane dotyczące nieprzewidzianych oddziałów z Instrukcji obsługi optymalizacji Intel [1]. Zwróć uwagę, że kara za nieprzewidziane rozgałęzienia jest zależna od procesora, jak zobaczymy w instrukcji Intela.

  4. Na koniec rozwiąż problemy zidentyfikowane przez profilera. Wiele technik zostało już tutaj omówionych. Dostępnych jest również wiele dobrych, niezawodnych i kompleksowych zasobów dotyczących optymalizacji. Aby wymienić tylko dwa, istnieje Podręcznik Intel Optimization Reference [1] oraz pięć podręczników optymalizacji autorstwa Agner Fog [2]. Zauważ, że są pewne rzeczy, których nie musisz robić, jeśli kompilator już to robi - na przykład rozwijanie pętli, wyrównywanie pamięci itp. Przeczytaj uważnie dokumentację kompilatora.

Bibliografia:

[1] Podręcznik informacyjny dotyczący optymalizacji architektury Intel 64 i IA-32: http://www.intel.sg/content/dam/doc/manual/64-ia-32-architectures-optimization-manual.pdf

[2] Agner Fog, „Software Optimization Resources”: http://www.agner.org/optimize/

  • „Optymalizacja oprogramowania w C ++: przewodnik po optymalizacji dla platform Windows, Linux i Mac”
  • „Optymalizacja podprogramów w języku asemblera: przewodnik po optymalizacji dla platform x86”
  • „Mikroarchitektura procesorów Intel, AMD i VIA: przewodnik po optymalizacji dla programistów a kompilatorów”
  • „Tabele instrukcji: Listy opóźnień instrukcji, przepustowości i awarii mikroprocesorów dla procesorów Intel, AMD i VIA”
  • „Konwencje wywoływania dla różnych kompilatorów C ++ i systemów operacyjnych”

3

Nie jestem naukowcem obliczeniowym, jak wielu innych tutaj (więc mogę się mylić :)), ale w dzisiejszych czasach nie ma sensu spędzać zbyt wiele czasu na wydajności szeregowej, o ile używamy standardowych bibliotek. Bardziej opłacalne może być poświęcenie dodatkowego czasu / wysiłku na zwiększenie skalowalności kodu.

W każdym razie tutaj są dwa przykłady (jeśli jeszcze ich nie przeczytałeś) dotyczące poprawy wydajności (w przypadku niestrukturalnych problemów z FE).

Numer seryjny : patrz 2. połowa tekstu streszczenia i pokrewnych.

Równolegle : Zwłaszcza faza inicjalizacji, w punkcie 4.2.


3

To może bardziej meta-odpowiedź niż odpowiedź ...

Musisz pogłębić znajomość swojego kompilatora. Możesz to najskuteczniej uzyskać, czytając instrukcję i eksperymentując z opcjami.

Wiele dobrych rad, które @Pedro wydaje, można wdrożyć dostosowując kompilację, a nie program.


Nie zgadzam się z ostatnim punktem. Wiedza o tym, co potrafi kompilator, to jedno, ale napisanie kodu, aby kompilator mógł coś z tym zrobić, to zupełnie inny problem. Nie ma flag kompilatora, które posortowałyby dane za Ciebie, używają mniejszej precyzji, gdy jest to wymagane, lub ponownie zapisują najbardziej wewnętrzne pętle, tak aby miały niewiele gałęzi lub nie miały ich wcale. Znajomość kompilatora to dobra rzecz, ale pomoże ci tylko napisać lepszy kod, ale nie poprawi kodu jako takiego.
Pedro

1

Prostym sposobem profilowania programu (w systemie Linux) jest użycie go perfw stattrybie. Najprostszym sposobem jest po prostu uruchomienie

perf stat ./my_program args ...

i da ci wiele przydatnych statystyk wydajności:

Performance counter stats for './simd_test1':

     3884.559489 task-clock                #    1.000 CPUs utilized
              18 context-switches          #    0.005 K/sec
               0 cpu-migrations            #    0.000 K/sec
             383 page-faults               #    0.099 K/sec
  10,911,904,779 cycles                    #    2.809 GHz
 <not supported> stalled-cycles-frontend
 <not supported> stalled-cycles-backend
  14,346,983,161 instructions              #    1.31  insns per cycle
   2,143,017,630 branches                  #  551.676 M/sec
          28,892 branch-misses             #    0.00% of all branches

     3.885986246 seconds time elapsed

Czasami wyświetla również listę obciążeń i braków pamięci podręcznej D. Jeśli zauważysz wiele braków pamięci podręcznej, oznacza to, że Twój program intensywnie zapełnia pamięć i źle traktuje pamięć podręczną. Obecnie procesory stają się szybsze niż przepustowość pamięci i zwykle problemem jest zawsze dostęp do pamięci.

Możesz także wypróbować perf record ./my_program; perf reportprosty sposób profilowania. Przeczytaj strony podręcznika, aby dowiedzieć się więcej.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.