Jak mogę profilować kod C ++ działający w systemie Linux?


1815

Mam aplikację C ++ działającą w systemie Linux, którą właśnie optymalizuję. Jak mogę wskazać, które obszary mojego kodu działają wolno?


27
Jeśli dostarczysz więcej danych na temat stosu programistycznego, możesz uzyskać lepsze odpowiedzi. Istnieją profilery od Intel i Sun, ale musisz użyć ich kompilatorów. Czy to jest opcja?
Nazgob,

2
Odpowiedź jest już dostępna pod następującym linkiem: stackoverflow.com/questions/2497211/...
Kapil Gupta

4
Większość odpowiedzi to osoby codeprofilujące. Jednak odwrócenie priorytetu, aliasing pamięci podręcznej, rywalizacja o zasoby itp. Mogą być czynnikami wpływającymi na optymalizację i wydajność. Myślę, że ludzie czytają informacje w moim wolnym kodzie . FAQ odnoszą się do tego wątku.
artless noise


3
Kiedyś używałem pstacka losowo, przez większość czasu drukuję najbardziej typowy stos, w którym program jest przez większość czasu, stąd wskazując na wąskie gardło.
Jose Manuel Gomez Alvarez

Odpowiedzi:


1405

Jeśli Twoim celem jest użycie profilera, użyj jednego z sugerowanych.

Jeśli jednak się spieszysz i możesz ręcznie przerwać program w debuggerze, gdy jest subiektywnie wolny, istnieje prosty sposób na znalezienie problemów z wydajnością.

Po prostu zatrzymaj go kilka razy i za każdym razem spójrz na stos wywołań. Jeśli istnieje kod, który marnuje pewien procent czasu, 20% lub 50% lub cokolwiek innego, jest to prawdopodobieństwo, że złapiesz go w akcie na każdej próbce. Jest to więc mniej więcej procent próbek, na których go zobaczysz. Nie wymaga wykształconego zgadywania. Jeśli zgadniesz, na czym polega problem, to go udowodni lub obali.

Możesz mieć wiele problemów z wydajnością o różnych rozmiarach. Jeśli wyczyścisz którekolwiek z nich, pozostałe zajmą większy procent i będą łatwiejsze do wykrycia przy kolejnych podaniach. Ten efekt powiększenia w połączeniu z wieloma problemami może prowadzić do naprawdę ogromnych czynników przyspieszenia.

Uwaga : programiści zwykle sceptycznie podchodzą do tej techniki, chyba że sami jej użyli. Powiedzą, że profilerzy podają te informacje, ale jest to prawdą tylko wtedy, gdy próbkują cały stos wywołań, a następnie pozwalają zbadać losowy zestaw próbek. (Streszczenia są tam, gdzie tracony jest wgląd.) Wykresy połączeń nie podają takich samych informacji, ponieważ

  1. Nie podsumowują na poziomie instrukcji, i
  2. Dają mylące streszczenia w obecności rekurencji.

Powiedzą także, że działa tylko na programach zabawkowych, podczas gdy w rzeczywistości działa na dowolnym programie i wydaje się, że działa lepiej na większych programach, ponieważ mają one więcej problemów ze znalezieniem. Mówią, że czasami znajduje rzeczy, które nie są problemami, ale jest to prawdą tylko wtedy, gdy raz coś zobaczysz . Jeśli widzisz problem na więcej niż jednej próbce, jest on prawdziwy.

PS Można to również zrobić w programach wielowątkowych, jeśli istnieje sposób na zbieranie próbek stosu wywołań puli wątków w danym momencie, tak jak ma to miejsce w Javie.

PPS Ogólnie rzecz biorąc, im więcej warstw abstrakcji masz w swoim oprogramowaniu, tym większe jest prawdopodobieństwo, że jest to przyczyną problemów z wydajnością (i szansą na przyspieszenie).

Dodany : Może to nie być oczywiste, ale technika próbkowania stosu działa równie dobrze w obecności rekurencji. Powodem jest to, że czas, który można by zaoszczędzić na usunięciu instrukcji, jest przybliżany przez ułamek próbek, które ją zawierają, niezależnie od tego, ile razy może wystąpić w próbce.

Inny zarzut, który często słyszę, brzmi: „ Zatrzyma się gdzieś losowo i umknie prawdziwemu problemowi ”. Wynika to z uprzedniej koncepcji prawdziwego problemu. Kluczową właściwością problemów z wydajnością jest to, że przekraczają oczekiwania. Pobieranie próbek mówi ci, że coś jest problemem, a twoją pierwszą reakcją jest niedowierzanie. To naturalne, ale możesz być pewien, że jeśli znajdziesz problem, to jest prawdziwy i na odwrót.

Dodano : Pozwól mi wyjaśnić bayesowskie wyjaśnienie, jak to działa. Załóżmy, że jest jakaś instrukcja I(wywołanie lub inna), która znajduje się na stosie wywołania przez ułamek fczasu (a zatem kosztuje tyle). Dla uproszczenia załóżmy, że nie wiemy, co to fjest, ale załóżmy, że jest to 0,1, 0,2, 0,3, ... 0,9, 1,0, a wcześniejsze prawdopodobieństwo każdej z tych możliwości wynosi 0,1, więc wszystkie te koszty są jednakowo prawdopodobne a-priori

Załóżmy, że pobieramy tylko 2 próbki stosu i widzimy instrukcje Ina obu próbach, wyznaczoną obserwację o=2/2. To daje nam nowe szacunki częstotliwości fod I, zgodnie z tym:

Prior                                    
P(f=x) x  P(o=2/2|f=x) P(o=2/2&&f=x)  P(o=2/2&&f >= x)  P(f >= x | o=2/2)

0.1    1     1             0.1          0.1            0.25974026
0.1    0.9   0.81          0.081        0.181          0.47012987
0.1    0.8   0.64          0.064        0.245          0.636363636
0.1    0.7   0.49          0.049        0.294          0.763636364
0.1    0.6   0.36          0.036        0.33           0.857142857
0.1    0.5   0.25          0.025        0.355          0.922077922
0.1    0.4   0.16          0.016        0.371          0.963636364
0.1    0.3   0.09          0.009        0.38           0.987012987
0.1    0.2   0.04          0.004        0.384          0.997402597
0.1    0.1   0.01          0.001        0.385          1

                  P(o=2/2) 0.385                

Ostatnia kolumna mówi, że na przykład prawdopodobieństwo, że f> = 0,5 wynosi 92%, w porównaniu z wcześniejszym założeniem 60%.

Załóżmy, że wcześniejsze założenia są różne. Załóżmy, że zakładamy P(f=0.1)0,991 (prawie pewne), a wszystkie inne możliwości są prawie niemożliwe (0,001). Innymi słowy, naszą wcześniejszą pewnością jest to, że Ijest tanie. Następnie otrzymujemy:

Prior                                    
P(f=x) x  P(o=2/2|f=x) P(o=2/2&& f=x)  P(o=2/2&&f >= x)  P(f >= x | o=2/2)

0.001  1    1              0.001        0.001          0.072727273
0.001  0.9  0.81           0.00081      0.00181        0.131636364
0.001  0.8  0.64           0.00064      0.00245        0.178181818
0.001  0.7  0.49           0.00049      0.00294        0.213818182
0.001  0.6  0.36           0.00036      0.0033         0.24
0.001  0.5  0.25           0.00025      0.00355        0.258181818
0.001  0.4  0.16           0.00016      0.00371        0.269818182
0.001  0.3  0.09           0.00009      0.0038         0.276363636
0.001  0.2  0.04           0.00004      0.00384        0.279272727
0.991  0.1  0.01           0.00991      0.01375        1

                  P(o=2/2) 0.01375                

Teraz mówi, że P(f >= 0.5)wynosi 26%, w porównaniu z wcześniejszym założeniem 0,6%. Tak więc Bayes pozwala nam zaktualizować nasze oszacowanie prawdopodobnego kosztu I. Jeśli ilość danych jest niewielka, nie mówi nam dokładnie, jaki jest koszt, tylko tyle, że jest wystarczająco duża, aby warto ją naprawić.

Jeszcze inny sposób spojrzenia na to nazywa się Regułą Sukcesji . Jeśli rzucisz monetą 2 razy i za każdym razem wyjdzie ona na głowę, co to mówi o prawdopodobnym ważeniu monety? Szacowanym sposobem odpowiedzi jest stwierdzenie, że jest to rozkład Beta o średniej wartości (number of hits + 1) / (number of tries + 2) = (2+1)/(2+2) = 75%.

(Kluczem jest to, że widzimy Iwięcej niż jeden raz. Jeśli widzimy to tylko raz, to niewiele nam mówi, z wyjątkiem tego, że f> 0.)

Tak więc nawet bardzo mała liczba próbek może nam wiele powiedzieć o koszcie instrukcji, które widzi. (I to zobaczyć je z częstotliwością średnio proporcjonalny do ich kosztu. Jeżeli npobierane są próbki, a fto koszt, a następnie Ipojawi się na nf+/-sqrt(nf(1-f))próbkach. Przykład, n=10, f=0.3, czyli 3+/-1.4próbki).


Dodano : Aby intuicyjnie wyczuć różnicę między pomiarem a losowym próbkowaniem stosu:
istnieją profilery, które próbkują stos, nawet w czasie zegara ściennego, ale wychodzi z tego pomiary (lub gorąca ścieżka lub punkt zapalny, z którego „wąskie gardło” można łatwo ukryć). To, czego ci nie pokazują (i z łatwością mogą), to same próbki. A jeśli Twoim celem jest znalezienie wąskiego gardła, ich liczba to średnio 2 podzielone przez ułamek czasu. Więc jeśli zajmie to 30% czasu, pokaże to średnio 2 / .3 = 6,7 próbek, a szansa, że ​​20 próbek wykaże, że wynosi 99,2%.

Oto nieszablonowa ilustracja różnicy między badaniem pomiarów a badaniem próbek stosu. Wąskim gardłem może być jedna taka duża kropelka lub wiele małych, to nie ma znaczenia.

wprowadź opis zdjęcia tutaj

Pomiar jest poziomy; informuje, jaki ułamek czasu zajmują określone procedury. Próbkowanie jest pionowe. Jeśli jest jakiś sposób na uniknięcie tego, co robi cały program w tym momencie, i jeśli zobaczysz to na drugiej próbce , to znalazłeś wąskie gardło. To właśnie robi różnicę - widząc cały powód spędzonego czasu, a nie tylko ile.


292
Jest to w zasadzie profiler pobierania próbek od biednego człowieka, co jest świetne, ale ryzykujesz zbyt małą próbką, która może dać całkowicie fałszywe wyniki.
Crashworks

100
@ Crash: Nie będę debatował na temat „biedaka” :-) To prawda, że ​​dokładność pomiarów statystycznych wymaga wielu próbek, ale są dwa sprzeczne cele - pomiar i lokalizacja problemu. Skupiam się na tym drugim, dla którego potrzebujesz precyzji lokalizacji, a nie precyzji pomiaru. Na przykład może istnieć jedno-wywołanie funkcji A () na środkowym stosie; stanowi to 50% czasu, ale może być w innej dużej funkcji B, wraz z wieloma innymi wywołaniami A (), które nie są kosztowne. Dokładne podsumowanie czasów funkcji może być wskazówką, ale każda inna próbka stosu wskaże problem.
Mike Dunlavey

41
... świat wydaje się uważać, że wykres połączeń, opatrzony adnotacjami z liczbą połączeń i / lub średnim czasem, jest wystarczająco dobry. Nie jest. A smutne jest to, że dla tych, którzy próbkują stos wywołań, najbardziej użyteczna informacja jest tuż przed nimi, ale wyrzucają ją w interesie „statystyki”.
Mike Dunlavey

30
Nie chcę się nie zgadzać z twoją techniką. Najwyraźniej polegam dość mocno na profilujących próbkach. Po prostu zwracam uwagę, że istnieją narzędzia, które robią to teraz w sposób zautomatyzowany, co jest ważne, gdy przekroczysz próg uzyskania funkcji z 25% do 15% i musisz ją obniżyć z 1,2% do 0,6%
Crashworks

13
-1: Świetny pomysł, ale jeśli zarabiasz za pracę nawet w środowisku umiarkowanie wydajnym, jest to strata czasu każdego. Użyj prawdziwego profilera, abyśmy nie musieli iść za tobą i naprawiać rzeczywistych problemów.
Sam Harwell

583

Możesz użyć Valgrind z następującymi opcjami

valgrind --tool=callgrind ./(Your binary)

Wygeneruje plik o nazwie callgrind.out.x. Następnie możesz użyć kcachegrindnarzędzia do odczytania tego pliku. To da ci graficzną analizę rzeczy z wynikami, takimi jak które linie kosztują ile.


51
valgrind jest świetny, ale ostrzegam, że spowolni twój program
neves

30
Sprawdź także Gprof2Dot, aby uzyskać niesamowity alternatywny sposób wizualizacji wyników. ./gprof2dot.py -f callgrind callgrind.out.x | dot -Tsvg -o output.svg
Sebastian

2
@neves Tak Valgrind jest po prostu niezbyt pomocny pod względem prędkości do profilowania aplikacji „gstreamer” i „opencv” w czasie rzeczywistym.
entuzjastyczny

1
stackoverflow.com/questions/375913/... jest częściowym rozwiązaniem problemu prędkości.
Tõnu Samuel

3
@Sebastian: gprof2dotjest teraz tutaj: github.com/jrfonseca/gprof2dot
John Zwinck

347

Zakładam, że używasz GCC. Standardowym rozwiązaniem byłoby profilowanie za pomocą gprof .

Pamiętaj, aby dodać -pgdo kompilacji przed profilowaniem:

cc -o myprog myprog.c utils.c -g -pg

Jeszcze tego nie próbowałem, ale słyszałem dobre rzeczy o Google-perftools . Zdecydowanie warto spróbować.

Powiązane pytanie tutaj .

Kilka innych modnych haseł, jeśli gprofnie spełnia Twoich wymagań : Valgrind , Intel VTune , Sun DTrace .


3
Zgadzam się, że gprof jest obecnym standardem. Tylko uwaga: Valgrind służy do profilowania wycieków pamięci i innych aspektów związanych z pamięcią programów, a nie do optymalizacji prędkości.
Bill the Lizard,

68
Bill, W pakiecie vaglrind można znaleźć callgrind i masyw. Oba są bardzo przydatne do profilowania aplikacji
dario minonne

7
@ Bill-the-Lizard: Kilka komentarzy na temat gprof : stackoverflow.com/questions/1777556/alternatives-to-gprof/…
Mike Dunlavey

6
gprof -pg jest jedynie przybliżeniem profilu stosu wywołań. Wstawia wywołania mcount, aby śledzić, które funkcje wywołują, które inne funkcje. Używa standardowego próbkowania na podstawie czasu. Następnie przydziela czasy próbkowane w funkcji foo () z powrotem do wywołujących foo (), proporcjonalnie do liczby wywołań. Więc nie rozróżnia połączeń różnych kosztów.
Krazy Glew

1
W przypadku clang / clang ++ można rozważyć użycie profilera procesora gperftools . Uwaga: sam tego nie zrobiłem.
einpoklum

257

Nowsze jądra (np. Najnowsze jądra Ubuntu) są wyposażone w nowe narzędzia „perf” ( apt-get install linux-tools) AKA perf_events .

Są one wyposażone w klasyczne profile do próbkowania ( strona man ), a także niesamowity timechart !

Ważną rzeczą jest to, że narzędzia te mogą być profilowaniem systemu, a nie tylko profilowaniem procesów - mogą pokazywać interakcje między wątkami, procesami i jądrem oraz pozwalają zrozumieć zależności planowania i we / wy między procesami.

Tekst alternatywny


12
Świetne narzędzie! Czy w ogóle mogę uzyskać typowy widok „motyla”, który zaczyna się w stylu „main-> func1-> fun2”? Nie potrafię tego perf report
rozgryźć

Will, może perf pokaż wykres czasowy aktywności wątku; z dodanymi informacjami o numerze procesora? Chcę zobaczyć, kiedy i który wątek działał na każdym procesorze.
osgx,

2
@ kizzx2 - możesz używać gprof2doti perf script. Bardzo fajne narzędzie!
dashy

2
Nawet nowsze jądra, takie jak 4.13, mają profil eBPF. Zobacz brendangregg.com/blog/2015-05-15/ebpf-one-small-step.html i brendangregg.com/ebpf.html
Andrew Stern

Kolejne miłe wprowadzenie do perfistnieje na stronie archive.li/9r927#selection-767.126-767.271 (Dlaczego bogowie SO postanowili usunąć tę stronę z bazy wiedzy SO jest poza mną ....)
ragerdl

75

Używałbym Valgrind i Callgrind jako podstawy mojego pakietu narzędzi do profilowania. Ważne jest, aby wiedzieć, że Valgrind jest w zasadzie maszyną wirtualną:

(wikipedia) Valgrind jest w istocie maszyną wirtualną wykorzystującą techniki kompilacji just-in-time (JIT), w tym rekompilację dynamiczną. Nic z oryginalnego programu nigdy nie jest uruchamiane bezpośrednio na procesorze hosta. Zamiast tego Valgrind najpierw tłumaczy program na tymczasową, prostszą formę o nazwie Intermediate Representation (IR), która jest niezależna od procesora, oparta na SSA. Po konwersji narzędzie (patrz poniżej) może swobodnie dokonywać dowolnych transformacji na IR, zanim Valgrind przetłumaczy IR z powrotem na kod maszynowy i pozwoli procesorowi hosta go uruchomić.

Callgrind to oparty na nim profiler. Główną zaletą jest to, że nie trzeba uruchamiać aplikacji przez wiele godzin, aby uzyskać wiarygodny wynik. Nawet jedna sekunda jest wystarczająca, aby uzyskać solidne, wiarygodne wyniki, ponieważ Callgrind jest profilerem bez sondowania .

Kolejnym narzędziem zbudowanym na Valgrind jest Massif. Używam go do profilowania zużycia pamięci sterty. Działa świetnie. To, co robi, zapewnia migawki użycia pamięci - szczegółowe informacje CO zawiera CO NAJWYŻSZY procent pamięci, a KTO ją tam umieścił. Takie informacje są dostępne w różnych momentach uruchamiania aplikacji.


70

Odpowiedź na bieg valgrind --tool=callgrind nie jest kompletna bez niektórych opcji. Zazwyczaj nie chcemy profilować 10 minut wolnego czasu uruchamiania w Valgrind i chcemy profilować nasz program, gdy wykonuje jakieś zadanie.

Tak właśnie polecam. Najpierw uruchom program:

valgrind --tool=callgrind --dump-instr=yes -v --instr-atstart=no ./binary > tmp

Teraz, gdy to działa i chcemy rozpocząć profilowanie, powinniśmy uruchomić w innym oknie:

callgrind_control -i on

Włącza profilowanie. Aby go wyłączyć i zatrzymać całe zadanie, możemy użyć:

callgrind_control -k

Teraz mamy kilka plików o nazwie callgrind.out. * W bieżącym katalogu. Aby zobaczyć wyniki profilowania, użyj:

kcachegrind callgrind.out.*

Polecam w następnym oknie kliknąć nagłówek kolumny „Self”, w przeciwnym razie pokazuje, że „main ()” jest najbardziej czasochłonnym zadaniem. „Ja” pokazuje, ile każda funkcja wymagała czasu, a nie razem z osobami na utrzymaniu.


9
Z jakiegoś powodu pliki callgrind.out. * Były zawsze puste. Wykonanie callgrind_control -d było przydatne do wymuszenia zrzutu danych na dysk.
Tõnu Samuel

3
Żargon. Moje zwykłe konteksty to coś w rodzaju całego MySQL lub PHP lub czegoś podobnego. Często nawet nie wiem na początku, co chcę rozdzielić.
Tõnu Samuel

2
Lub w moim przypadku mój program faktycznie ładuje wiązkę danych do pamięci podręcznej LRU i nie chcę tego profilować. Więc wymuszam ładowanie podzbioru pamięci podręcznej podczas uruchamiania i profilowanie kodu przy użyciu tylko tych danych (pozwalając, aby procesor OS + CPU zarządzał wykorzystaniem pamięci w mojej pamięci podręcznej). Działa, ale ładowanie pamięci podręcznej jest powolne, a procesor intensywny w całym kodzie, który próbuję profilować w innym kontekście, więc callgrind produkuje bardzo zanieczyszczone wyniki.
Kod Abominator

2
istnieje również CALLGRIND_TOGGLE_COLLECTprogramowe włączenie / wyłączenie gromadzenia; patrz stackoverflow.com/a/13700817/288875
Andre Holzner

1
Wow, nie wiedziałem, że to istnieje, dzięki!
Vincent Fourmond,

59

Jest to odpowiedź na odpowiedź Gprof firmy Nazgob .

Używam Gprof w ciągu ostatnich kilku dni i już znalazłem trzy znaczące ograniczenia, z których jednego nie widziałem (nigdzie indziej) udokumentowanego:

  1. Nie działa poprawnie na kodzie wielowątkowym, chyba że zastosujesz obejście

  2. Wykres wywołania jest mylony przez wskaźniki funkcji. Przykład: Mam wywoływaną funkcję, multithread()która pozwala mi na wielowątkowanie określonej funkcji na określonej tablicy (obie przekazywane jako argumenty). Gprof uważa jednak wszystkie połączenia multithread()za równoważne w celu obliczenia czasu spędzonego u dzieci. Ponieważ niektóre funkcje przechodzę multithread()znacznie dłużej niż inne, moje wykresy połączeń są w większości bezużyteczne. (Dla tych, którzy zastanawiają się, czy problemem jest tutaj multithread()wątek : nie, można opcjonalnie iw tym przypadku uruchomić wszystko sekwencyjnie tylko na wątku wywołującym).

  3. To mówi tutaj, że „... liczby połączeń są obliczane na podstawie liczenia, a nie próbkowania. Są one całkowicie dokładne…”. Jednak mój wykres wywołań daje mi 5345859132 + 784984078 jako statystyki wywołań mojej najczęściej wywoływanej funkcji, gdzie pierwszy numer ma być połączeniami bezpośrednimi, a drugi wywołaniami rekurencyjnymi (wszystkie są od siebie). Ponieważ sugerowało to, że mam błąd, umieściłem długie (64-bitowe) liczniki w kodzie i powtórzyłem to samo. Moje liczą się: 5345859132 bezpośrednie i 78094395406 samorekurencyjne połączenia. Jest tam dużo cyfr, więc wskazuję, że rekurencyjne połączenia, które mierzę, wynoszą 78 miliardów w porównaniu z 784m od Gprof: współczynnik 100 różnych. Oba przebiegi były jednowątkowym i niezoptymalizowanym kodem, jeden skompilowany, -ga drugi -pg.

To był GNU Gprof (GNU Binutils dla Debiana) 2.18.0.20080103 działający pod 64-bitowym systemem Debian Lenny, jeśli to pomaga komukolwiek.


Tak, wykonuje próbkowanie, ale nie dotyczy liczb połączeń. Co ciekawe, podążając za linkiem ostatecznie doprowadziłem mnie do zaktualizowanej wersji strony podręcznika, do której odsyłam w swoim poście, nowy adres URL: sourceware.org/binutils/docs/gprof/... To powtórzy cytat w części (iii) mojej odpowiedzi, ale także mówi: „W aplikacjach wielowątkowych lub aplikacjach jednowątkowych, które łączą się z bibliotekami wielowątkowymi, liczby są deterministyczne tylko wtedy, gdy funkcja liczenia jest bezpieczna dla wątków. (Uwaga: uwaga: funkcja liczenia mcount w glibc nie jest wątkowa -bezpieczny)."
Rob_before_edits

Nie jest dla mnie jasne, czy to wyjaśnia mój wynik w (iii). Mój kod został połączony -lpthread -lm i zadeklarowałem zarówno zmienną statyczną „pthread_t * thr”, jak i „pthread_mutex_t nextLock = PTHREAD_MUTEX_INITIALIZER”, nawet gdy działał jednowątkowy. Zazwyczaj zakładałbym, że „połączenie z bibliotekami wielowątkowymi” oznacza faktycznie korzystanie z tych bibliotek i w większym stopniu niż to, ale mogę się mylić!
Rob_before_edits

23

Użyj Valgrind, callgrind i kcachegrind:

valgrind --tool=callgrind ./(Your binary)

generuje callgrind.out.x. Przeczytaj to za pomocą kcachegrind.

Użyj gprof (dodaj -pg):

cc -o myprog myprog.c utils.c -g -pg 

(niezbyt dobre dla wielu wątków, wskaźników funkcji)

Użyj Google-perftools:

Wykorzystuje próbkowanie czasu, ujawniono wąskie gardła we / wy i procesora.

Intel VTune jest najlepszy (darmowy do celów edukacyjnych).

Inne: AMD Codeanalyst (od czasu zastąpienia AMD CodeXL), OProfile, narzędzia „perf” (apt-get install Linux-tools)


10

Przegląd technik profilowania w C ++

W tej odpowiedzi użyję kilku różnych narzędzi do analizy kilku bardzo prostych programów testowych, aby konkretnie porównać działanie tych narzędzi.

Poniższy program testowy jest bardzo prosty i wykonuje następujące czynności:

  • mainpołączenia fasti maybe_slow3 razy, jeden zmaybe_slow jest wolne

    Powolne wywołanie maybe_slowjest 10 razy dłuższe i dominuje w środowisku wykonawczym, jeśli weźmiemy pod uwagę wywołania funkcji potomnej common. Idealnie, narzędzie do profilowania będzie w stanie wskazać nam konkretne wolne połączenie.

  • zarówno fasti maybe_slowcall common, co stanowi większość wykonania programu

  • Interfejs programu to:

    ./main.out [n [seed]]

    a program O(n^2)w sumie wykonuje pętle. seedjest po prostu uzyskać inną wydajność bez wpływu na środowisko uruchomieniowe.

main.c

#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>

uint64_t __attribute__ ((noinline)) common(uint64_t n, uint64_t seed) {
    for (uint64_t i = 0; i < n; ++i) {
        seed = (seed * seed) - (3 * seed) + 1;
    }
    return seed;
}

uint64_t __attribute__ ((noinline)) fast(uint64_t n, uint64_t seed) {
    uint64_t max = (n / 10) + 1;
    for (uint64_t i = 0; i < max; ++i) {
        seed = common(n, (seed * seed) - (3 * seed) + 1);
    }
    return seed;
}

uint64_t __attribute__ ((noinline)) maybe_slow(uint64_t n, uint64_t seed, int is_slow) {
    uint64_t max = n;
    if (is_slow) {
        max *= 10;
    }
    for (uint64_t i = 0; i < max; ++i) {
        seed = common(n, (seed * seed) - (3 * seed) + 1);
    }
    return seed;
}

int main(int argc, char **argv) {
    uint64_t n, seed;
    if (argc > 1) {
        n = strtoll(argv[1], NULL, 0);
    } else {
        n = 1;
    }
    if (argc > 2) {
        seed = strtoll(argv[2], NULL, 0);
    } else {
        seed = 0;
    }
    seed += maybe_slow(n, seed, 0);
    seed += fast(n, seed);
    seed += maybe_slow(n, seed, 1);
    seed += fast(n, seed);
    seed += maybe_slow(n, seed, 0);
    seed += fast(n, seed);
    printf("%" PRIX64 "\n", seed);
    return EXIT_SUCCESS;
}

gprof

gprof wymaga ponownej kompilacji oprogramowania z oprzyrządowaniem, a także stosuje podejście próbkowania wraz z oprzyrządowaniem. Dlatego zachowuje równowagę między dokładnością (próbkowanie nie zawsze jest w pełni dokładne i może pomijać funkcje) a spowolnieniem wykonywania (oprzyrządowanie i próbkowanie są stosunkowo szybkimi technikami, które nie spowalniają zbytnio wykonywania).

gprof jest wbudowany w GCC / binutils, więc wystarczy skompilować z -pgopcją włączenia gprof. Następnie uruchamiamy program normalnie z parametrem CLI rozmiaru, który generuje rozsądny czas trwania kilku sekund ( 10000):

gcc -pg -ggdb3 -O3 -std=c99 -Wall -Wextra -pedantic -o main.out main.c
time ./main.out 10000

Ze względów edukacyjnych przeprowadzimy również bieg bez włączonych optymalizacji. Pamiętaj, że jest to bezużyteczne w praktyce, ponieważ zwykle zależy Ci tylko na optymalizacji wydajności zoptymalizowanego programu:

gcc -pg -ggdb3 -O0 -std=c99 -Wall -Wextra -pedantic -o main.out main.c
./main.out 10000

Po pierwsze, timemówi nam, że czas wykonania zi bez niego -pgbył taki sam, co jest świetne: bez spowolnienia! Widziałem jednak konta 2x - 3x spowolnienia w złożonym oprogramowaniu, np. Jak pokazano na tym bilecie .

Ponieważ się skompilowaliśmy -pg, uruchomienie programu powoduje utworzenie gmon.outpliku zawierającego dane profilowania.

Możemy obserwować ten plik graficznie za pomocą gprof2dotpytania: Czy można uzyskać graficzną reprezentację wyników gprof?

sudo apt install graphviz
python3 -m pip install --user gprof2dot
gprof main.out > main.gprof
gprof2dot < main.gprof | dot -Tsvg -o output.svg

Tutaj gprofnarzędzie odczytuje gmon.outinformacje o śledzeniu i generuje raport czytelny dla człowieka main.gprof, który gprof2dotnastępnie czyta w celu wygenerowania wykresu.

Źródło gprof2dot znajduje się na stronie : https://github.com/jrfonseca/gprof2dot

Podczas biegu obserwujemy -O0:

wprowadź opis zdjęcia tutaj

i na -O3bieg:

wprowadź opis zdjęcia tutaj

Wynik -O0jest dość oczywisty. Na przykład pokazuje, że 3 maybe_slowwywołania i ich wywołania podrzędne zajmują 97,56% całkowitego czasu wykonywania, chociaż maybe_slowsamo wykonanie bez dzieci stanowi 0,00% całkowitego czasu wykonywania, tj. Prawie cały czas spędzony na tej funkcji połączenia z dziećmi.

DO ZROBIENIA: dlaczego mainbrakuje w -O3danych wyjściowych, mimo że widzę to na btGDB? Brakuje mi funkcji wyjściowej GProf. Myślę, że dzieje się tak, ponieważ gprof opiera się na próbkowaniu oprócz skompilowanego oprzyrządowania, a -O3 mainjest po prostu zbyt szybki i nie ma próbek.

Wybieram wyjście SVG zamiast PNG, ponieważ SVG można przeszukiwać za pomocą Ctrl + F, a rozmiar pliku może być około 10 razy mniejszy. Ponadto szerokość i wysokość generowanego obrazu może być humoungous z dziesiątkami tysięcy pikseli dla złożonego oprogramowania, eogaw tym przypadku GNOME 3.28.1 zawiera błędy w przypadku plików PNG, podczas gdy pliki SVG są otwierane automatycznie przez moją przeglądarkę. gimp 2.8 działał jednak dobrze, zobacz także:

ale nawet wtedy będziesz dużo przeciągał obraz, aby znaleźć to, czego szukasz, patrz np. ten obraz z „prawdziwego” przykładu oprogramowania wziętego z tego biletu :

wprowadź opis zdjęcia tutaj

Czy potrafisz łatwo znaleźć najbardziej krytyczny stos wywołań, gdy wszystkie te maleńkie nieposortowane linie spaghetti nachodzą na siebie? dotJestem pewien, że mogą być lepsze opcje, ale nie chcę teraz tam iść. To, czego naprawdę potrzebujemy, to odpowiednia dedykowana przeglądarka, ale jeszcze jej nie znalazłem:

Możesz jednak użyć mapy kolorów, aby nieco złagodzić te problemy. Na przykład na poprzednim ogromnym obrazie w końcu udało mi się znaleźć krytyczną ścieżkę po lewej stronie, kiedy dokonałem genialnego wniosku, że zieleń pojawia się po czerwieni, a następnie ciemniej i ciemniej niebieski.

Alternatywnie możemy również obserwować tekst wyjściowy gprofwbudowanego narzędzia binutils, które wcześniej zapisaliśmy:

cat main.gprof

Domyślnie powoduje to bardzo szczegółowe wyjście, które wyjaśnia, co oznaczają dane wyjściowe. Ponieważ nie potrafię tego lepiej wyjaśnić, pozwolę ci to przeczytać sam.

Po zrozumieniu formatu wyjściowego danych możesz zmniejszyć gadatliwość, aby wyświetlać tylko dane bez samouczka z -bopcją:

gprof -b main.out

W naszym przykładzie wyniki dotyczyły -O0:

Flat profile:

Each sample counts as 0.01 seconds.
  %   cumulative   self              self     total           
 time   seconds   seconds    calls   s/call   s/call  name    
100.35      3.67     3.67   123003     0.00     0.00  common
  0.00      3.67     0.00        3     0.00     0.03  fast
  0.00      3.67     0.00        3     0.00     1.19  maybe_slow

            Call graph


granularity: each sample hit covers 2 byte(s) for 0.27% of 3.67 seconds

index % time    self  children    called     name
                0.09    0.00    3003/123003      fast [4]
                3.58    0.00  120000/123003      maybe_slow [3]
[1]    100.0    3.67    0.00  123003         common [1]
-----------------------------------------------
                                                 <spontaneous>
[2]    100.0    0.00    3.67                 main [2]
                0.00    3.58       3/3           maybe_slow [3]
                0.00    0.09       3/3           fast [4]
-----------------------------------------------
                0.00    3.58       3/3           main [2]
[3]     97.6    0.00    3.58       3         maybe_slow [3]
                3.58    0.00  120000/123003      common [1]
-----------------------------------------------
                0.00    0.09       3/3           main [2]
[4]      2.4    0.00    0.09       3         fast [4]
                0.09    0.00    3003/123003      common [1]
-----------------------------------------------

Index by function name

   [1] common                  [4] fast                    [3] maybe_slow

i dla -O3:

Flat profile:

Each sample counts as 0.01 seconds.
  %   cumulative   self              self     total           
 time   seconds   seconds    calls  us/call  us/call  name    
100.52      1.84     1.84   123003    14.96    14.96  common

            Call graph


granularity: each sample hit covers 2 byte(s) for 0.54% of 1.84 seconds

index % time    self  children    called     name
                0.04    0.00    3003/123003      fast [3]
                1.79    0.00  120000/123003      maybe_slow [2]
[1]    100.0    1.84    0.00  123003         common [1]
-----------------------------------------------
                                                 <spontaneous>
[2]     97.6    0.00    1.79                 maybe_slow [2]
                1.79    0.00  120000/123003      common [1]
-----------------------------------------------
                                                 <spontaneous>
[3]      2.4    0.00    0.04                 fast [3]
                0.04    0.00    3003/123003      common [1]
-----------------------------------------------

Index by function name

   [1] common

Jako bardzo szybkie podsumowanie każdej sekcji, np .:

                0.00    3.58       3/3           main [2]
[3]     97.6    0.00    3.58       3         maybe_slow [3]
                3.58    0.00  120000/123003      common [1]

skupia się wokół funkcji, która jest wcięta ( maybe_flow). [3]jest identyfikatorem tej funkcji. Powyżej funkcji znajdują się jej osoby wywołujące, a poniżej osoby odbierające.

Dla -O3zobacz tutaj jak w twórczości graficznej, że maybe_slowi fastnie ma znanego rodzica, który jest co dokumentacja mówi, że <spontaneous>środki.

Nie jestem pewien, czy istnieje fajny sposób na profilowanie linia po linii za pomocą gprof: czas `gprof` spędzony w poszczególnych liniach kodu

valgrind callgrind

valgrind uruchamia program przez maszynę wirtualną valgrind. To sprawia, że ​​profilowanie jest bardzo dokładne, ale powoduje również bardzo duże spowolnienie programu. Wspominałem także wcześniej o kcachegrind: Narzędzia do uzyskania graficznego wywołania funkcji w kodzie kodu

callgrind to narzędzie valgrind do profilowania kodu, a kcachegrind to program KDE, który może wizualizować dane wyjściowe cachegrind.

Najpierw musimy usunąć -pgflagę, aby wrócić do normalnej kompilacji, w przeciwnym razie uruchomienie nie powiedzie sięProfiling timer expired , i tak, jest to tak częste, że zrobiłem to i pojawiło się pytanie o przepełnienie stosu.

Więc kompilujemy i uruchamiamy jako:

sudo apt install kcachegrind valgrind
gcc -ggdb3 -O3 -std=c99 -Wall -Wextra -pedantic -o main.out main.c
time valgrind --tool=callgrind valgrind --dump-instr=yes \
  --collect-jumps=yes ./main.out 10000

Włączam --dump-instr=yes --collect-jumps=yes ponieważ powoduje to również zrzut informacji, które pozwalają nam zobaczyć rozkład wydajności na linię montażową, przy stosunkowo niewielkich dodatkowych kosztach ogólnych.

Nietoperz timemówi nam, że wykonanie programu zajęło 29,5 sekundy, więc na tym przykładzie spowolniliśmy około 15x. Oczywiście spowolnienie będzie poważnym ograniczeniem dla większych obciążeń. Na wspomnianym tutaj „prawdziwym przykładzie oprogramowania” zaobserwowałem spowolnienie 80x.

Uruchomienie generuje plik danych profilu o nazwie callgrind.out.<pid>np. callgrind.out.8554W moim przypadku. Ten plik przeglądamy za pomocą:

kcachegrind callgrind.out.8554

który pokazuje graficzny interfejs użytkownika zawierający dane podobne do tekstowego wyniku gprof:

wprowadź opis zdjęcia tutaj

Ponadto, jeśli przejdziemy do zakładki „Call Graph” w prawym dolnym rogu, zobaczymy wykres połączeń, który możemy wyeksportować, klikając go prawym przyciskiem myszy, aby uzyskać następujący obraz z nieuzasadnioną ilością białej ramki :-)

wprowadź opis zdjęcia tutaj

Myślę, że fastnie pokazuje się na tym wykresie, ponieważ kcachegrind musiał uprościć wizualizację, ponieważ to wywołanie zajmuje zbyt mało czasu, prawdopodobnie takie zachowanie będzie pożądane w prawdziwym programie. Menu prawego kliknięcia ma pewne ustawienia kontrolujące, kiedy należy wyciąć takie węzły, ale po szybkiej próbie nie udało mi się wyświetlić tak krótkiego połączenia. Jeśli kliknę na fastlewe okno, wyświetli się wykres wywołań z fast, więc stos został przechwycony. Nikt jeszcze nie znalazł sposobu na wyświetlenie pełnego wykresu wywołań graficznych: Spraw, aby callgrind wyświetlał wszystkie wywołania funkcji w grafie wywołań kcachegrind

TODO na złożonym oprogramowaniu C ++, widzę niektóre wpisy typu <cycle N>, np. <cycle 11>Gdzie spodziewałbym się nazw funkcji, co to znaczy? Zauważyłem, że jest przycisk „Wykrywanie cyklu”, który włącza i wyłącza tę funkcję, ale co to oznacza?

perf od linux-tools

perfwydaje się używać wyłącznie mechanizmów próbkowania jądra Linux. To sprawia, że ​​konfiguracja jest bardzo prosta, ale nie do końca dokładna.

sudo apt install linux-tools
time perf record -g ./main.out 10000

To dodało 0.2s do wykonania, więc jesteśmy w porządku, ale nadal nie widzę większego zainteresowania, po rozwinięciu commonwęzła za pomocą prawej strzałki na klawiaturze:

Samples: 7K of event 'cycles:uppp', Event count (approx.): 6228527608     
  Children      Self  Command   Shared Object     Symbol                  
-   99.98%    99.88%  main.out  main.out          [.] common              
     common                                                               
     0.11%     0.11%  main.out  [kernel]          [k] 0xffffffff8a6009e7  
     0.01%     0.01%  main.out  [kernel]          [k] 0xffffffff8a600158  
     0.01%     0.00%  main.out  [unknown]         [k] 0x0000000000000040  
     0.01%     0.00%  main.out  ld-2.27.so        [.] _dl_sysdep_start    
     0.01%     0.00%  main.out  ld-2.27.so        [.] dl_main             
     0.01%     0.00%  main.out  ld-2.27.so        [.] mprotect            
     0.01%     0.00%  main.out  ld-2.27.so        [.] _dl_map_object      
     0.01%     0.00%  main.out  ld-2.27.so        [.] _xstat              
     0.00%     0.00%  main.out  ld-2.27.so        [.] __GI___tunables_init
     0.00%     0.00%  main.out  [unknown]         [.] 0x2f3d4f4944555453  
     0.00%     0.00%  main.out  [unknown]         [.] 0x00007fff3cfc57ac  
     0.00%     0.00%  main.out  ld-2.27.so        [.] _start              

Następnie próbuję przeprowadzić test porównawczy -O0programu, aby zobaczyć, czy to coś pokazuje, i dopiero teraz widzę wykres połączeń:

Samples: 15K of event 'cycles:uppp', Event count (approx.): 12438962281   
  Children      Self  Command   Shared Object     Symbol                  
+   99.99%     0.00%  main.out  [unknown]         [.] 0x04be258d4c544155  
+   99.99%     0.00%  main.out  libc-2.27.so      [.] __libc_start_main   
-   99.99%     0.00%  main.out  main.out          [.] main                
   - main                                                                 
      - 97.54% maybe_slow                                                 
           common                                                         
      - 2.45% fast                                                        
           common                                                         
+   99.96%    99.85%  main.out  main.out          [.] common              
+   97.54%     0.03%  main.out  main.out          [.] maybe_slow          
+    2.45%     0.00%  main.out  main.out          [.] fast                
     0.11%     0.11%  main.out  [kernel]          [k] 0xffffffff8a6009e7  
     0.00%     0.00%  main.out  [unknown]         [k] 0x0000000000000040  
     0.00%     0.00%  main.out  ld-2.27.so        [.] _dl_sysdep_start    
     0.00%     0.00%  main.out  ld-2.27.so        [.] dl_main             
     0.00%     0.00%  main.out  ld-2.27.so        [.] _dl_lookup_symbol_x 
     0.00%     0.00%  main.out  [kernel]          [k] 0xffffffff8a600158  
     0.00%     0.00%  main.out  ld-2.27.so        [.] mmap64              
     0.00%     0.00%  main.out  ld-2.27.so        [.] _dl_map_object      
     0.00%     0.00%  main.out  ld-2.27.so        [.] __GI___tunables_init
     0.00%     0.00%  main.out  [unknown]         [.] 0x552e53555f6e653d  
     0.00%     0.00%  main.out  [unknown]         [.] 0x00007ffe1cf20fdb  
     0.00%     0.00%  main.out  ld-2.27.so        [.] _start              

DO ZROBIENIA: co się stało z -O3egzekucją? Jest to po prostu, że maybe_slowi fastzbyt szybko i nie dostać żadnych próbek? Czy działa dobrze z -O3większymi programami, których uruchomienie trwa dłużej? Czy przegapiłem opcję CLI? Dowiedziałem się, że -Fkontroluję częstotliwość próbkowania w hercach, ale podniosłem ją do maksymalnej dozwolonej domyślnie wartości -F 39500((można ją zwiększyć sudo)) i nadal nie widzę wyraźnych połączeń.

Jedną fajną rzeczą perfjest narzędzie FlameGraph firmy Brendan Gregg, które wyświetla czasy stosów wywołań w bardzo zgrabny sposób, który pozwala szybko zobaczyć duże połączenia. Narzędzie jest dostępne na stronie : https://github.com/brendangregg/FlameGraph i jest również wspomniane w jego samouczku perf pod adresem : http://www.brendangregg.com/perf.html#FlameGraphs Kiedy biegałem perfbez sudo, dostałem ERROR: No stack counts foundto teraz zrobię to z sudo:

git clone https://github.com/brendangregg/FlameGraph
sudo perf record -F 99 -g -o perf_with_stack.data ./main.out 10000
sudo perf script -i perf_with_stack.data | FlameGraph/stackcollapse-perf.pl | FlameGraph/flamegraph.pl > flamegraph.svg

ale w tak prostym programie wynik nie jest bardzo łatwy do zrozumienia, ponieważ nie możemy łatwo zobaczyć maybe_slowani fasttego wykresu:

wprowadź opis zdjęcia tutaj

Na bardziej złożonym przykładzie staje się jasne, co oznacza wykres:

wprowadź opis zdjęcia tutaj

DO ZROBIENIA W tym przykładzie znajduje się dziennik [unknown]funkcji, dlaczego tak jest?

Inne interfejsy perf GUI, które mogą być tego warte, to:

  • Wtyczka Eclipse Trace Compass: https://www.eclipse.org/tracecompass/

    Ma to jednak tę wadę, że należy najpierw przekonwertować dane do formatu Common Trace Format, co można zrobić perf data --to-ctf, ale należy je włączyć w czasie kompilacji / mieć perfwystarczająco dużo nowych, z których żaden nie dotyczy perf Ubuntu 18.04

  • https://github.com/KDAB/hotspot

    Minusem tego jest to, że wydaje się, że nie ma pakietu Ubuntu, a jego zbudowanie wymaga Qt 5.10, podczas gdy Ubuntu 18.04 jest w wersji Qt 5.9.

gperftools

Wcześniej nazywany „Google Performance Tools”, źródło: https://github.com/gperftools/gperftools podstawie próbki.

Najpierw zainstaluj gperftools z:

sudo apt install google-perftools

Następnie możemy włączyć profiler CPU gperftools na dwa sposoby: w czasie wykonywania lub w czasie kompilacji.

W czasie wykonywania musimy przekazać ustawienie LD_PRELOADto point, do libprofiler.soktórego można znaleźć locate libprofiler.sonp. W moim systemie:

gcc -ggdb3 -O3 -std=c99 -Wall -Wextra -pedantic -o main.out main.c
LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libprofiler.so \
  CPUPROFILE=prof.out ./main.out 10000

Alternatywnie możemy zbudować bibliotekę w czasie łącza, rezygnując z przekazywania LD_PRELOADw czasie wykonywania:

gcc -Wl,--no-as-needed,-lprofiler,--as-needed -ggdb3 -O3 -std=c99 -Wall -Wextra -pedantic -o main.out main.c
CPUPROFILE=prof.out ./main.out 10000

Zobacz także: gperftools - plik profilu nie został zrzucony

Najlepszym sposobem na obejrzenie tych danych, które do tej pory znalazłem, jest ustawienie formatu wyjściowego pprof na taki sam format, jaki przyjmuje kcachegrind jako dane wejściowe (tak, narzędzie Valgrind-project-viewer-narzędzie) i użycie kcachegrind, aby wyświetlić:

google-pprof --callgrind main.out prof.out  > callgrind.out
kcachegrind callgrind.out

Po uruchomieniu jedną z tych metod otrzymujemy prof.outplik danych profilu jako wynik. Możemy wyświetlić ten plik graficznie jako plik SVG za pomocą:

google-pprof --web main.out prof.out

wprowadź opis zdjęcia tutaj

co daje znany wykres wywołań jak inne narzędzia, ale z nieporęczną jednostką liczby próbek zamiast sekund.

Alternatywnie możemy również uzyskać dane tekstowe za pomocą:

google-pprof --text main.out prof.out

co daje:

Using local file main.out.
Using local file prof.out.
Total: 187 samples
     187 100.0% 100.0%      187 100.0% common
       0   0.0% 100.0%      187 100.0% __libc_start_main
       0   0.0% 100.0%      187 100.0% _start
       0   0.0% 100.0%        4   2.1% fast
       0   0.0% 100.0%      187 100.0% main
       0   0.0% 100.0%      183  97.9% maybe_slow

Zobacz także: Jak korzystać z narzędzi Google perf

Testowany w Ubuntu 18.04, gprof2dot 2019.11.30, valgrind 3.13.0, perf 4.15.18, jądro Linux 4.15.0, FLameGraph 1a0dc6985aad06e76857cf2a354bd5ba0c9ce96b, gperftools 2.5-2.


2
Domyślnie perf record używa rejestru wskaźnika ramki. Nowoczesne kompilatory nie rejestrują adresu ramki i zamiast tego używają rejestru jako ogólnego przeznaczenia. Alternatywą jest kompilacja z -fno-omit-frame-pointerflagą lub użycie innej alternatywy: nagrywanie z --call-graph "dwarf"lub w --call-graph "lbr"zależności od scenariusza.
Jorge Bellon

5

W przypadku programów jednowątkowych możesz użyć igprof , The Ignominous Profiler: https://igprof.org/ .

Jest to profiler do próbkowania, zgodny z ... długą ... odpowiedzią Mike'a Dunlavey'a, który podaruje wyniki opakowanemu drzewku stosów wywołań do przeglądania, z adnotacjami o czasie lub pamięci spędzonej w każdej funkcji, kumulatywnej lub na funkcję.


Wygląda interesująco, ale nie można go skompilować z GCC 9.2. (Debian / Sid) Zrobiłem problem na github.
Basile Starynkevitch

5

Warto również wspomnieć

  1. HPCToolkit ( http://hpctoolkit.org/ ) - Open-source, działa dla programów równoległych i posiada GUI, za pomocą którego można przeglądać wyniki na wiele sposobów
  2. Intel VTune ( https://software.intel.com/en-us/vtune ) - Jeśli masz kompilatory Intel, jest to bardzo dobre
  3. TAU ( http://www.cs.uoregon.edu/research/tau/home.php )

Użyłem HPCToolkit i VTune i są one bardzo skuteczne w znajdowaniu długiego bieguna w namiocie i nie wymagają rekompilacji kodu (z wyjątkiem tego, że musisz użyć kompilacji typu -g -O lub RelWithDebInfo w CMake, aby uzyskać znaczące wyniki) . Słyszałem, że TAU ma podobne możliwości.


4

Są to dwie metody, których używam do przyspieszenia mojego kodu:

W przypadku aplikacji związanych z procesorem:

  1. Użyj narzędzia do profilowania w trybie DEBUG, aby zidentyfikować wątpliwe części kodu
  2. Następnie przejdź do trybu RELEASE i skomentuj wątpliwe sekcje kodu (usuń go z niczym), aż zobaczysz zmiany w wydajności.

W przypadku aplikacji związanych z We / Wy:

  1. Użyj narzędzia do profilowania w trybie RELEASE, aby zidentyfikować wątpliwe części kodu.

NB

Jeśli nie masz profilera, użyj profilera biedaka. Naciśnij pauzę podczas debugowania aplikacji. Większość pakietów programistów rozpadnie się na zespoły z komentarzem numerów linii. Statystycznie prawdopodobne jest wylądowanie w regionie, który zjada większość cykli procesora.

W przypadku procesora powodem profilowania w trybie DEBUGOWANIA jest to, że jeśli próbowałeś profilowania w trybie RELEASE , kompilator zredukuje matematykę, wektoryzuje pętle i funkcje wbudowane, które mają tendencję do przekształcania kodu w nieporządkowany bałagan po jego złożeniu. Nieodwzorowalny bałagan oznacza, że ​​Twój profiler nie będzie w stanie jasno określić, co zajmuje tak długo, ponieważ zestaw może nie odpowiadać optymalizowanemu kodowi źródłowemu . Jeśli potrzebujesz wydajności (np. Zależnej od czasu) trybu RELEASE , wyłącz funkcje debuggera, aby zachować użyteczną wydajność.

W przypadku powiązania we / wy profiler może nadal identyfikować operacje we / wy w trybie RELEASE , ponieważ operacje we / wy są albo zewnętrznie połączone z biblioteką współużytkowaną (przez większość czasu), albo w najgorszym przypadku mogą spowodować sys- wektor przerwań wywołania (który jest również łatwo rozpoznawalny przez profilera).


2
+1 Metoda biedaka działa tak samo dobrze dla powiązania I / O, jak i dla procesora, i zalecam wykonanie dostrajania wydajności w trybie DEBUG. Po zakończeniu strojenia włącz RELEASE. Poprawi się, jeśli program będzie związany z procesorem w kodzie. Oto prymitywne, ale krótkie wideo z tego procesu.
Mike Dunlavey

3
Nie używałbym kompilacji DEBUG do profilowania wydajności. Często widziałem, że części krytyczne pod względem wydajności w trybie DEBUG są całkowicie zoptymalizowane w trybie zwolnienia. Innym problemem jest użycie w kodzie debugowania asercji, które zwiększają wydajność.
gast128

3
Czy w ogóle czytałeś mój post? „Jeśli potrzebujesz wydajności (np. Wrażliwej na czas) trybu RELEASE, wyłącz funkcje debuggera, aby zachować użyteczną wydajność”, „Następnie przejdź do trybu RELEASE i skomentuj wątpliwe sekcje kodu (Stub go bez niczego), aż zobaczysz zmiany w wydajności. ”? Powiedziałem, aby sprawdzić możliwe obszary problemów w trybie debugowania i zweryfikować te problemy w trybie zwolnienia, aby uniknąć wspomnianego pułapki.
seo


2

Możesz użyć struktury rejestrowania, loguruponieważ zawiera ona znaczniki czasu i całkowity czas działania, który można ładnie wykorzystać do profilowania:


1

W pracy mamy naprawdę fajne narzędzie, które pomaga nam monitorować to, czego chcemy w zakresie planowania. Było to przydatne wiele razy.

Jest w C ++ i musi być dostosowany do twoich potrzeb. Niestety nie mogę udostępniać kodu, tylko koncepcje. Używasz „dużego” volatilebufora zawierającego znaczniki czasu i identyfikator zdarzenia, które możesz zrzucić po sekcji zwłok lub po zatrzymaniu systemu rejestrowania (i na przykład zrzucić je do pliku).

Pobierasz tak zwany duży bufor ze wszystkimi danymi, a mały interfejs analizuje go i wyświetla zdarzenia o nazwie (góra / dół + wartość), tak jak oscyloskop z kolorami (skonfigurowany w .hpppliku).

Dostosowujesz liczbę generowanych zdarzeń, aby koncentrować się wyłącznie na tym, czego pragniesz. Bardzo nam to pomogło w problemach z planowaniem, zużywając potrzebną ilość procesora na podstawie liczby zarejestrowanych zdarzeń na sekundę.

Potrzebujesz 3 plików:

toolname.hpp // interface
toolname.cpp // code
tool_events_id.hpp // Events ID

Koncepcja polega na zdefiniowaniu zdarzeń w tool_events_id.hppten sposób:

// EVENT_NAME                         ID      BEGIN_END BG_COLOR NAME
#define SOCK_PDU_RECV_D               0x0301  //@D00301 BGEEAAAA # TX_PDU_Recv
#define SOCK_PDU_RECV_F               0x0302  //@F00301 BGEEAAAA # TX_PDU_Recv

Możesz także zdefiniować kilka funkcji w toolname.hpp:

#define LOG_LEVEL_ERROR 0
#define LOG_LEVEL_WARN 1
// ...

void init(void);
void probe(id,payload);
// etc

Gdziekolwiek w twoim kodzie możesz użyć:

toolname<LOG_LEVEL>::log(EVENT_NAME,VALUE);

probeWykorzystuje się kilka linii produkcyjnych do pobierania znacznik czasu zegara jak najszybciej, a następnie ustawia się wpis w buforze. Mamy również przyrost atomowy, aby bezpiecznie znaleźć indeks, w którym należy zapisać zdarzenie dziennika. Oczywiście bufor jest okrągły.

Mam nadzieję, że pomysł nie jest zaciemniony przez brak przykładowego kodu.


1

Właściwie to trochę zaskoczony, niewiele wspomniało o google / benchmarku , podczas gdy przypinanie określonego obszaru kodu jest trochę kłopotliwe, szczególnie jeśli baza kodu jest trochę duża, ale znalazłem to naprawdę pomocne, gdy używa się go w połączeniu zcallgrind

Kluczem tutaj jest identyfikacja IMHO elementu, który powoduje wąskie gardło. Najpierw jednak spróbuję odpowiedzieć na poniższe pytania i na tej podstawie wybrać narzędzie

  1. czy mój algorytm jest poprawny?
  2. czy są zamki, które okazują się być szyjkami butelek?
  3. czy jest jakaś konkretna sekcja kodu, która okazuje się być winowajcą?
  4. co powiesz na IO, obsługiwane i zoptymalizowane?

valgrindz kombinacją callrindi kcachegrindpowinien zapewnić przyzwoitą ocenę powyższych punktów, a gdy okaże się, że występują problemy z pewną sekcją kodu, sugeruję, aby zrobić mikroprocesor google benchmarkto dobre miejsce na początek.


1

Użyj -pgflagi podczas kompilowania i łączenia kodu i uruchom plik wykonywalny. Podczas wykonywania tego programu dane profilowania są gromadzone w pliku a.out.
Istnieją dwa różne typy profilowania

1- Płaskie profilowanie:
uruchamiając polecenie gprog --flat-profile a.outotrzymujesz następujące dane
- jaki procent całkowitego czasu spędzonego na funkcji,
- ile sekund spędzono w funkcji - w tym i wyłączając wywołania podfunkcji,
- liczbę połączenia,
- średni czas na połączenie.

2- wykres profilujący
nam polecenie, gprof --graph a.outaby uzyskać następujące dane dla każdej funkcji, która obejmuje:
- W każdej sekcji jedna funkcja jest oznaczona numerem indeksu.
- Nad funkcją znajduje się lista funkcji, które ją wywołują.
- Poniżej funkcji znajduje się lista funkcji wywoływanych przez funkcję.

Aby uzyskać więcej informacji, zajrzyj na https://sourceware.org/binutils/docs-2.32/gprof/


0

Jak nikt nie wspomniał o Arm MAP, dodałbym go, ponieważ osobiście z powodzeniem użyłem Map do profilowania programu naukowego C ++.

Arm MAP jest profilerem dla równoległych, wielowątkowych lub jednowątkowych kodów C, C ++, Fortran i F90. Zapewnia dogłębną analizę i precyzyjne określenie wąskiego gardła do linii źródłowej. W przeciwieństwie do większości profilerów, jest on zaprojektowany do profilowania pthreads, OpenMP lub MPI dla kodu równoległego i wątkowego.

MAP to oprogramowanie komercyjne.


0

używać oprogramowania do debugowania, jak rozpoznać, gdzie kod działa wolno?

pomyśl tylko, że masz przeszkodę podczas ruchu, a wtedy zmniejszysz prędkość

takie jak zapętlanie niechcianych realokacji, przepełnienie bufora, wyszukiwanie, wycieki pamięci itp. operacje zużywają więcej mocy wykonawczej, wpłynie to niekorzystnie na wydajność kodu, pamiętaj o dodaniu -pg do kompilacji przed profilowaniem:

g++ your_prg.cpp -pglub cc my_program.cpp -g -pgzgodnie z kompilatorem

jeszcze tego nie próbowałem, ale słyszałem dobre rzeczy o Google-perftools. Zdecydowanie warto spróbować.

valgrind --tool=callgrind ./(Your binary)

Wygeneruje plik o nazwie gmon.out lub callgrind.out.x. Następnie możesz użyć narzędzia kcachegrind lub debuggera do odczytania tego pliku. To da ci graficzną analizę rzeczy z wynikami, takimi jak które linie kosztują ile.

Chyba tak

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.