Übersicht über C ++ - Profiling-Techniken
In dieser Antwort werde ich verschiedene Tools verwenden, um einige sehr einfache Testprogramme zu analysieren und die Funktionsweise dieser Tools konkret zu vergleichen.
Das folgende Testprogramm ist sehr einfach und führt Folgendes aus:
main
Anrufe fast
und maybe_slow
3 Mal, wobei einer der maybe_slow
Anrufe langsam ist
Der langsame Aufruf von maybe_slow
ist 10x länger und dominiert die Laufzeit, wenn wir Aufrufe der untergeordneten Funktion berücksichtigen common
. Im Idealfall kann das Profiling-Tool uns auf den spezifischen langsamen Aufruf verweisen.
beides fast
und maybe_slow
call common
, was den Großteil der Programmausführung ausmacht
Die Programmoberfläche ist:
./main.out [n [seed]]
und das Programm macht O(n^2)
insgesamt Schleifen. seed
dient nur dazu, eine andere Ausgabe zu erhalten, ohne die Laufzeit zu beeinträchtigen.
Haupt 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 erfordert das Neukompilieren der Software mit Instrumentierung und verwendet zusammen mit dieser Instrumentierung einen Stichprobenansatz. Es wird daher ein Gleichgewicht zwischen Genauigkeit (Abtastung ist nicht immer vollständig genau und kann Funktionen überspringen) und Ausführungsverlangsamung (Instrumentierung und Abtastung sind relativ schnelle Techniken, die die Ausführung nicht sehr verlangsamen) hergestellt.
gprof ist in GCC / binutils integriert. Wir müssen also nur mit der -pg
Option kompilieren , um gprof zu aktivieren. Wir führen das Programm dann normal mit einem CLI-Parameter der Größe aus, der einen Lauf mit einer angemessenen Dauer von einigen Sekunden erzeugt ( 10000
):
gcc -pg -ggdb3 -O3 -std=c99 -Wall -Wextra -pedantic -o main.out main.c
time ./main.out 10000
Aus pädagogischen Gründen werden wir auch einen Lauf ohne aktivierte Optimierungen durchführen. Beachten Sie, dass dies in der Praxis nutzlos ist, da Sie normalerweise nur die Leistung des optimierten Programms optimieren möchten:
gcc -pg -ggdb3 -O0 -std=c99 -Wall -Wextra -pedantic -o main.out main.c
./main.out 10000
Erstens time
sagt uns, dass die Ausführungszeit mit und ohne -pg
gleich war, was großartig ist: keine Verlangsamung! Ich habe jedoch Berichte über 2x - 3x Verlangsamungen bei komplexer Software gesehen, z. B. wie in diesem Ticket gezeigt .
Da wir mit kompiliert haben -pg
, erzeugt das Ausführen des Programms eine Dateigmon.out
erstellt, die die Profildaten enthält.
Wir können diese Datei grafisch beobachten, indem wir gprof2dot
gefragt werden: Ist es möglich, eine grafische Darstellung der gprof-Ergebnisse zu erhalten?
sudo apt install graphviz
python3 -m pip install --user gprof2dot
gprof main.out > main.gprof
gprof2dot < main.gprof | dot -Tsvg -o output.svg
Hier gprof
liest das Tool die gmon.out
Trace-Informationen und generiert einen von Menschen lesbaren Bericht, in main.gprof
demgprof2dot
dann liest, um ein Diagramm zu generieren.
Die Quelle für gprof2dot ist: https://github.com/jrfonseca/gprof2dot
Wir beobachten Folgendes für den -O0
Lauf:
und für den -O3
Lauf:
Die -O0
Ausgabe ist ziemlich selbsterklärend. Zum Beispiel zeigt es, dass die 3 maybe_slow
Aufrufe und ihre untergeordneten Aufrufe 97,56% der gesamten Laufzeit ausmachen, obwohl die Ausführung maybe_slow
ohne untergeordnete Aufrufe 0,00% der gesamten Ausführungszeit ausmacht, dh fast die gesamte Zeit, die für diese Funktion aufgewendet wurde Kinderanrufe.
TODO: Warum main
fehlt in der -O3
Ausgabe, obwohl ich es auf einem bt
in GDB sehen kann? Fehlende Funktion in der GProf-Ausgabe Ich denke, das liegt daran, dass gprof neben seiner kompilierten Instrumentierung auch Sampling-basierte Funktionen verwendet. Das -O3
main
ist einfach zu schnell und hat keine Samples.
Ich wähle SVG-Ausgabe anstelle von PNG, da die SVG mit Strg + F durchsucht werden kann und die Dateigröße etwa 10x kleiner sein kann. Außerdem kann die Breite und Höhe des generierten Bildes mit Zehntausenden von Pixeln für komplexe Software eog
enorm sein , und GNOME 3.28.1 tritt in diesem Fall bei PNGs auf, während SVGs von meinem Browser automatisch geöffnet werden. Gimp 2.8 hat aber gut funktioniert, siehe auch:
Aber selbst dann ziehen Sie das Bild viel herum, um zu finden, was Sie wollen. Sehen Sie sich z. B. dieses Bild aus einem "echten" Softwarebeispiel aus diesem Ticket an :
Können Sie den kritischsten Anrufstapel leicht finden, wenn all diese winzigen unsortierten Spaghetti-Linien übereinander gehen? dot
Ich bin mir sicher, dass es bessere Optionen gibt, aber ich möchte jetzt nicht dorthin gehen. Was wir wirklich brauchen, ist ein engagierter Betrachter dafür, aber ich habe noch keinen gefunden:
Sie können jedoch die Farbkarte verwenden, um diese Probleme ein wenig zu verringern. Zum Beispiel habe ich es auf dem vorherigen großen Bild endlich geschafft, den kritischen Pfad auf der linken Seite zu finden, als ich den brillanten Schluss gezogen habe, dass Grün nach Rot kommt, gefolgt von immer dunklerem Blau.
Alternativ können wir auch die Textausgabe des gprof
integrierten binutils-Tools beobachten, das wir zuvor gespeichert haben unter:
cat main.gprof
Standardmäßig wird eine äußerst ausführliche Ausgabe erstellt, die erklärt, was die Ausgabedaten bedeuten. Da ich es nicht besser erklären kann, lasse ich Sie es selbst lesen.
Sobald Sie das Datenausgabeformat verstanden haben, können Sie die Ausführlichkeit reduzieren, um nur die Daten ohne das Lernprogramm mit der folgenden -b
Option anzuzeigen:
gprof -b main.out
In unserem Beispiel waren die Ausgaben für -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
und für -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
Als sehr schnelle Zusammenfassung für jeden Abschnitt zB:
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]
zentriert sich um die Funktion, die eingerückt bleibt ( maybe_flow
). [3]
ist die ID dieser Funktion. Über der Funktion befinden sich die Anrufer und darunter die Callees.
Für -O3
sehen, hier wie in der grafischen Ausgabe , dass maybe_slow
und fast
keinen bekannten Elternteil hat, das ist das, was in der Dokumentation, sagt <spontaneous>
Mittel.
Ich bin mir nicht sicher, ob es eine gute Möglichkeit gibt, mit gprof zeilenweise Profile zu erstellen: `gprof` Zeit, die in bestimmten Codezeilen verbracht wird
valgrind callgrind
valgrind führt das Programm über die virtuelle Maschine valgrind aus. Dies macht die Profilerstellung sehr genau, führt aber auch zu einer sehr starken Verlangsamung des Programms. Ich habe kcachegrind auch bereits erwähnt unter: Tools zum Abrufen eines Diagramms für Bildaufrufe von Code
callgrind ist das Werkzeug von valgrind zum Profilieren von Code und kcachegrind ist ein KDE-Programm, das die Cachegrind-Ausgabe visualisieren kann.
Zuerst müssen wir das -pg
Flag entfernen , um zur normalen Kompilierung zurückzukehren, andernfalls schlägt der Lauf tatsächlich mit fehlProfiling timer expired
, und ja, dies ist so häufig, dass ich es getan habe, und es gab eine Frage zum Stapelüberlauf.
Also kompilieren und laufen wir wie folgt:
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
Ich aktiviere --dump-instr=yes --collect-jumps=yes
weil dadurch auch Informationen ausgegeben werden, die es uns ermöglichen, eine Aufschlüsselung der Leistung pro Fließband bei relativ geringen zusätzlichen Gemeinkosten anzuzeigen.
Auf Anhieb wurde time
uns mitgeteilt, dass die Ausführung des Programms 29,5 Sekunden dauerte, sodass wir in diesem Beispiel eine etwa 15-fache Verlangsamung hatten. Diese Verlangsamung wird eindeutig eine ernsthafte Einschränkung für größere Workloads darstellen. Bei dem hier erwähnten "realen Softwarebeispiel" habe ich eine Verlangsamung von 80x beobachtet.
Der Lauf generiert eine Profildatendatei mit dem Namen callgrind.out.<pid>
zB callgrind.out.8554
in meinem Fall. Wir sehen diese Datei mit:
kcachegrind callgrind.out.8554
Dies zeigt eine GUI, die Daten enthält, die der Textausgabe von gprof ähneln:
Wenn wir unten rechts auf die Registerkarte "Anrufdiagramm" gehen, sehen wir ein Anrufdiagramm, das wir exportieren können, indem wir mit der rechten Maustaste darauf klicken, um das folgende Bild mit unangemessenen weißen Rändern zu erhalten :-)
Ich denke, fast
wird in diesem Diagramm nicht angezeigt, da kcachegrind die Visualisierung vereinfacht haben muss, da dieser Aufruf zu wenig Zeit in Anspruch nimmt. Dies ist wahrscheinlich das Verhalten, das Sie für ein echtes Programm wünschen. Das Rechtsklick-Menü enthält einige Einstellungen, mit denen gesteuert werden kann, wann solche Knoten entfernt werden sollen. Nach einem kurzen Versuch konnte ich jedoch keinen so kurzen Anruf anzeigen. Wenn ich fast
auf das linke Fenster klicke, wird ein Anrufdiagramm mit angezeigt fast
, sodass der Stapel tatsächlich erfasst wurde. Bisher hatte noch niemand eine Möglichkeit gefunden, das vollständige Diagrammaufrufdiagramm anzuzeigen : Lassen Sie callgrind alle Funktionsaufrufe im kcachegrind-Aufrufgraphen anzeigen
TODO auf komplexer C ++ - Software sehe ich einige Einträge vom Typ <cycle N>
, z. B. <cycle 11>
wo ich Funktionsnamen erwarten würde, was bedeutet das? Ich habe festgestellt, dass es eine Schaltfläche "Zykluserkennung" gibt, mit der Sie das ein- und ausschalten können. Aber was bedeutet das?
perf
von linux-tools
perf
scheint ausschließlich Linux-Kernel-Sampling-Mechanismen zu verwenden. Dies macht es sehr einfach einzurichten, aber auch nicht ganz genau.
sudo apt install linux-tools
time perf record -g ./main.out 10000
Dies hat die Ausführung um 0,2 Sekunden verlängert, so dass wir zeitlich in Ordnung sind, aber ich sehe immer noch kein großes Interesse, nachdem ich den common
Knoten mit dem rechten Pfeil der Tastatur erweitert habe:
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
Dann versuche ich, das -O0
Programm zu vergleichen, um festzustellen, ob dies etwas anzeigt, und erst jetzt sehe ich endlich ein Aufrufdiagramm:
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
TODO: Was ist bei der -O3
Hinrichtung passiert ? Ist es einfach so maybe_slow
und fast
waren zu schnell und haben keine Proben bekommen? Funktioniert es gut mit -O3
größeren Programmen, deren Ausführung länger dauert? Habe ich eine CLI-Option verpasst? Ich habe herausgefunden -F
, dass ich die Abtastfrequenz in Hertz steuern soll, aber ich habe sie auf das maximal zulässige Maximum von -F 39500
(könnte mit erhöht werden sudo
) eingestellt und sehe immer noch keine eindeutigen Anrufe.
Eine coole Sache perf
ist das FlameGraph-Tool von Brendan Gregg, das die Anrufstapel-Timings auf sehr übersichtliche Weise anzeigt, sodass Sie die großen Anrufe schnell sehen können. Das Tool ist verfügbar unter: https://github.com/brendangregg/FlameGraph und wird auch auf seinem perf Tutorial erwähnt unter: http://www.brendangregg.com/perf.html#FlameGraphs Als ich lief perf
ohne sudo
Ich habe ERROR: No stack counts found
so für jetzt mache ich es mit 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
aber in einem so einfachen Programm ist die Ausgabe nicht sehr leicht zu verstehen, da wir weder maybe_slow
noch fast
in dieser Grafik leicht sehen können :
An einem komplexeren Beispiel wird deutlich, was die Grafik bedeutet:
TODO gibt es ein Protokoll von [unknown]
in diesem Beispiel Funktionsprotokoll, warum ist das so?
Eine weitere perfekte GUI-Oberfläche, die sich lohnen könnte, sind:
Eclipse Trace Compass Plugin: https://www.eclipse.org/tracecompass/
Dies hat jedoch den Nachteil, dass Sie zuerst die Daten in das Common Trace-Format konvertieren müssen, was zwar möglich ist perf data --to-ctf
, aber zum Zeitpunkt der Erstellung aktiviert sein muss / perf
neu genug sein muss, was für die Ausführung nicht der Fall ist Ubuntu 18.04
https://github.com/KDAB/hotspot
Der Nachteil dabei ist, dass es anscheinend kein Ubuntu-Paket gibt und für dessen Erstellung Qt 5.10 erforderlich ist, während Ubuntu 18.04 bei Qt 5.9 ist.
gperftools
Zuvor als "Google Performance Tools" bezeichnet, Quelle: https://github.com/gperftools/gperftools Beispielbasiert.
Installieren Sie zuerst gperftools mit:
sudo apt install google-perftools
Anschließend können wir den gperftools-CPU-Profiler auf zwei Arten aktivieren: zur Laufzeit oder zur Erstellungszeit.
Zur Laufzeit müssen wir set the LD_PRELOAD
to point übergeben libprofiler.so
, mit dem Sie locate libprofiler.so
zB auf meinem System finden können:
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
Alternativ können wir die Bibliothek zur Verbindungszeit einbauen und LD_PRELOAD
zur Laufzeit auf Folgendes verzichten:
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
Siehe auch: gperftools - Profildatei nicht gesichert
Der schönste Weg, diese Daten anzuzeigen, die ich bisher gefunden habe, besteht darin, die Ausgabe von pprof auf dasselbe Format zu bringen, das kcachegrind als Eingabe verwendet (ja, das Valgrind-Projekt-Viewer-Tool), und kcachegrind zu verwenden, um Folgendes anzuzeigen:
google-pprof --callgrind main.out prof.out > callgrind.out
kcachegrind callgrind.out
Nach dem Ausführen mit einer dieser Methoden erhalten wir eine prof.out
Profildatendatei als Ausgabe. Wir können diese Datei grafisch als SVG anzeigen mit:
google-pprof --web main.out prof.out
Dies ergibt wie andere Tools ein vertrautes Aufrufdiagramm, jedoch mit der klobigen Einheit der Anzahl der Samples anstelle von Sekunden.
Alternativ können wir auch einige Textdaten erhalten mit:
google-pprof --text main.out prof.out
was gibt:
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
Siehe auch: So verwenden Sie Google Perf Tools
Getestet in Ubuntu 18.04, gprof2dot 2019.11.30, valgrind 3.13.0, perf 4.15.18, Linux-Kernel 4.15.0, FLameGraph 1a0dc6985aad06e76857cf2a354bd5ba0c9ce96b, gperftools 2.5-2.