Rilievo di tecniche di profilazione C ++
In questa risposta, userò diversi strumenti per analizzare alcuni programmi di test molto semplici, al fine di confrontare concretamente il funzionamento di tali strumenti.
Il seguente programma di test è molto semplice e procede come segue:
main
chiamate fast
e maybe_slow
3 volte, una delle maybe_slow
chiamate è lenta
La chiamata lenta di maybe_slow
è 10 volte più lunga e domina il runtime se consideriamo le chiamate alla funzione figlio common
. Idealmente, lo strumento di profilazione sarà in grado di indicarci la specifica chiamata lenta.
entrambi fast
e maybe_slow
call common
, che rappresenta la maggior parte dell'esecuzione del programma
L'interfaccia del programma è:
./main.out [n [seed]]
e il programma esegue O(n^2)
loop in totale. seed
è solo per ottenere output diversi senza influire sul runtime.
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 richiede la ricompilazione del software con la strumentazione e utilizza anche un approccio di campionamento insieme a quella strumentazione. Pertanto, trova un equilibrio tra accuratezza (il campionamento non è sempre del tutto accurato e può saltare le funzioni) e il rallentamento dell'esecuzione (la strumentazione e il campionamento sono tecniche relativamente veloci che non rallentano molto l'esecuzione).
gprof è integrato in GCC / binutils, quindi tutto ciò che dobbiamo fare è compilare con l' -pg
opzione per abilitare gprof. Quindi eseguiamo normalmente il programma con un parametro CLI size che produce una durata ragionevole di alcuni secondi ( 10000
):
gcc -pg -ggdb3 -O3 -std=c99 -Wall -Wextra -pedantic -o main.out main.c
time ./main.out 10000
Per motivi educativi, eseguiremo anche una corsa senza ottimizzazioni abilitate. Si noti che questo è inutile in pratica, poiché normalmente ti interessa solo ottimizzare le prestazioni del programma ottimizzato:
gcc -pg -ggdb3 -O0 -std=c99 -Wall -Wextra -pedantic -o main.out main.c
./main.out 10000
Innanzitutto, time
ci dice che i tempi di esecuzione con e senza -pg
erano gli stessi, il che è fantastico: nessun rallentamento! Ho comunque visto account di rallentamenti 2x - 3x su software complessi, ad esempio come mostrato in questo ticket .
Poiché abbiamo compilato -pg
, l'esecuzione del programma produce un file gmon.out
file contenente i dati di profilatura.
Possiamo osservare graficamente quel file con gprof2dot
la domanda: È possibile ottenere una rappresentazione grafica dei risultati di gprof?
sudo apt install graphviz
python3 -m pip install --user gprof2dot
gprof main.out > main.gprof
gprof2dot < main.gprof | dot -Tsvg -o output.svg
Qui, lo gprof
strumento legge le gmon.out
informazioni di traccia e genera un rapporto leggibile dall'uomo main.gprof
, che gprof2dot
quindi legge per generare un grafico.
La fonte per gprof2dot è a: https://github.com/jrfonseca/gprof2dot
Osserviamo quanto segue per la -O0
corsa:
e per la -O3
corsa:
L' -O0
output è praticamente autoesplicativo. Ad esempio, mostra che le 3 maybe_slow
chiamate e le loro chiamate figlio occupano il 97,56% del tempo di esecuzione totale, sebbene l'esecuzione di maybe_slow
se stessa senza figli rappresenti lo 0,00% del tempo di esecuzione totale, ovvero quasi tutto il tempo trascorso in quella funzione è stato impiegato bambino chiama.
TODO: perché main
manca l' -O3
output, anche se riesco a vederlo su un bt
in GDB? Funzione mancante dall'output GProf Penso che sia perché gprof è basato anche sul campionamento oltre alla sua strumentazione compilata, ed -O3
main
è semplicemente troppo veloce e non ha ottenuto campioni.
Ho scelto l'output SVG anziché PNG perché l'SVG è ricercabile con Ctrl + F e la dimensione del file può essere circa 10 volte più piccola. Inoltre, la larghezza e l'altezza dell'immagine generata possono essere humoungous con decine di migliaia di pixel per software complessi, e GNOME eog
3.28.1 in quel caso bug fuori per PNG, mentre gli SVG vengono aperti automaticamente dal mio browser. gimp 2.8 ha funzionato bene, vedi anche:
ma anche allora, trascinerai l'immagine molto per trovare quello che vuoi, vedi ad esempio questa immagine da un "vero" esempio di software preso da questo ticket :
Riesci a trovare facilmente lo stack di chiamate più critico con tutte quelle piccole linee di spaghetti non ordinate che si sovrappongono? Potrebbero esserci dot
opzioni migliori , ma non voglio andarci adesso. Ciò di cui abbiamo davvero bisogno è un visualizzatore dedicato adeguato, ma non ne ho ancora trovato uno:
Puoi tuttavia utilizzare la mappa dei colori per mitigare un po 'questi problemi. Ad esempio, nella precedente enorme immagine, sono finalmente riuscito a trovare il percorso critico sulla sinistra quando ho fatto la brillante deduzione che il verde viene dopo il rosso, seguito infine dal blu sempre più scuro.
In alternativa, possiamo anche osservare l'output di testo dello gprof
strumento binutils incorporato che in precedenza abbiamo salvato in:
cat main.gprof
Per impostazione predefinita, questo produce un output estremamente dettagliato che spiega cosa significano i dati di output. Dal momento che non posso spiegare meglio di così, ti lascerò leggerlo da solo.
Una volta compreso il formato di output dei dati, è possibile ridurre la verbosità per mostrare solo i dati senza il tutorial con l' -b
opzione:
gprof -b main.out
Nel nostro esempio, gli output sono stati per -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
e per -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
Come una sintesi molto rapida per ogni sezione, ad esempio:
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]
ruota attorno alla funzione che viene lasciata rientrata ( maybe_flow
). [3]
è l'ID di quella funzione. Sopra la funzione, ci sono i suoi chiamanti, e sotto di essa le chiamate.
Per -O3
, vedi qui come nell'output grafico che maybe_slow
e fast
non hanno un genitore noto, che è ciò che la documentazione dice che <spontaneous>
significa.
Non sono sicuro che esista un buon modo per eseguire il profiling riga per riga con gprof: il tempo `gprof` trascorso in particolari righe di codice
valgrind callgrind
valgrind esegue il programma attraverso la macchina virtuale valgrind. Ciò rende la profilazione molto accurata, ma produce anche un notevole rallentamento del programma. Ho già menzionato kcachegrind in precedenza in: Strumenti per ottenere un grafico di chiamata di funzione pittorica di codice
callgrind è lo strumento di valgrind per profilare il codice e kcachegrind è un programma KDE in grado di visualizzare l'output di cachegrind.
Per prima cosa dobbiamo rimuovere il -pg
flag per tornare alla compilazione normale, altrimenti la corsa in realtà fallisce Profiling timer expired
e sì, questo è così comune che l'ho fatto e c'era una domanda Stack Overflow per questo.
Quindi compiliamo ed eseguiamo come:
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
Abilito --dump-instr=yes --collect-jumps=yes
perché questo scarica anche le informazioni che ci consentono di visualizzare una suddivisione delle prestazioni per linea di assemblaggio, a un costo aggiuntivo aggiunto relativamente piccolo.
Detto questo, time
ci dice che l'esecuzione del programma ha richiesto 29,5 secondi, quindi in questo esempio abbiamo avuto un rallentamento di circa 15x. Chiaramente, questo rallentamento rappresenterà una seria limitazione per carichi di lavoro più grandi. Nell'esempio di "software reale" menzionato qui , ho osservato un rallentamento di 80x.
La corsa genera un file di dati del profilo chiamato callgrind.out.<pid>
ad esempio callgrind.out.8554
nel mio caso. Visualizziamo quel file con:
kcachegrind callgrind.out.8554
che mostra una GUI che contiene dati simili all'output testuale di gprof:
Inoltre, se andiamo nella scheda "Call Graph" in basso a destra, vediamo un grafico di chiamata che possiamo esportare facendo clic con il tasto destro del mouse per ottenere la seguente immagine con quantità irragionevoli di bordo bianco :-)
Penso che fast
non sia mostrato su quel grafico perché kcachegrind deve aver semplificato la visualizzazione perché quella chiamata impiega troppo poco tempo, questo sarà probabilmente il comportamento desiderato su un vero programma. Il menu di scelta rapida ha alcune impostazioni per controllare quando eliminare tali nodi, ma non sono riuscito a mostrare una chiamata così breve dopo un rapido tentativo. Se clicco sulla fast
finestra di sinistra, mostra un grafico di chiamata con fast
, quindi lo stack è stato effettivamente catturato. Nessuno aveva ancora trovato un modo per mostrare il grafico completo delle chiamate del grafico: Rendi callgrind mostra tutte le chiamate di funzione nel callgraph di kcachegrind
TODO su software C ++ complesso, vedo alcune voci di tipo <cycle N>
, ad esempio <cycle 11>
dove mi aspetto i nomi delle funzioni, cosa significa? Ho notato che c'è un pulsante "Cycle Detection" per attivarlo e disattivarlo, ma cosa significa?
perf
a partire dal linux-tools
perf
sembra usare esclusivamente meccanismi di campionamento del kernel Linux. Questo rende molto semplice l'installazione, ma anche non completamente accurato.
sudo apt install linux-tools
time perf record -g ./main.out 10000
Questo ha aggiunto 0,2 secondi all'esecuzione, quindi stiamo bene per quanto riguarda il tempo, ma non vedo ancora molto interesse, dopo aver espanso il common
nodo con la freccia destra della tastiera:
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
Quindi provo a confrontare il -O0
programma per vedere se mostra qualcosa e solo ora finalmente vedo un grafico di chiamata:
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: cosa è successo -O3
nell'esecuzione? È semplicemente così maybe_slow
ed fast
era troppo veloce e non ha ottenuto alcun campione? Funziona bene con -O3
programmi più grandi che richiedono più tempo per l'esecuzione? Ho perso qualche opzione CLI? Ho scoperto il -F
controllo della frequenza di campionamento in Hertz, ma l'ho alzato al massimo consentito di default -F 39500
(potrebbe essere aumentato con sudo
) e non vedo ancora chiamate chiare.
Una cosa interessante perf
è lo strumento FlameGraph di Brendan Gregg che mostra i tempi dello stack delle chiamate in un modo molto ordinato che ti consente di vedere rapidamente le grandi chiamate. Lo strumento è disponibile all'indirizzo: https://github.com/brendangregg/FlameGraph ed è menzionato anche il suo perf tutorial: http://www.brendangregg.com/perf.html#FlameGraphs Quando ho eseguito perf
senza sudo
ho avuto ERROR: No stack counts found
modo di ora lo farò con 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
ma in un programma così semplice l'output non è molto facile da capire, dal momento che non possiamo facilmente vedere maybe_slow
né fast
su quel grafico:
Sull'esempio più complesso diventa chiaro cosa significa il grafico:
TODO ci sono un registro di [unknown]
funzioni in quell'esempio, perché?
Altre interfacce GUI perf che potrebbero valerne la pena includono:
Plug-in Eclipse Trace Compass: https://www.eclipse.org/tracecompass/
Ma questo ha il rovescio della medaglia che devi prima convertire i dati nel Common Trace Format, che può essere fatto con perf data --to-ctf
, ma deve essere abilitato al momento della compilazione / avere perf
abbastanza nuovo, nessuno dei due non è il caso del perf in Ubuntu 18.04
https://github.com/KDAB/hotspot
Il rovescio della medaglia è che non sembra esserci alcun pacchetto Ubuntu, e la sua costruzione richiede Qt 5.10 mentre Ubuntu 18.04 è a Qt 5.9.
gperftools
Precedentemente chiamato "Google Performance Tools", fonte: https://github.com/gperftools/gperftools basato su campioni.
Prima installa gperftools con:
sudo apt install google-perftools
Quindi, possiamo abilitare il profiler CPU gperftools in due modi: in fase di esecuzione o in fase di compilazione.
In fase di esecuzione, dobbiamo passare impostare il LD_PRELOAD
punto to libprofiler.so
, che puoi trovare con locate libprofiler.so
, ad esempio sul mio sistema:
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
In alternativa, possiamo creare la libreria al momento del collegamento, dispensando il passaggio LD_PRELOAD
in fase di esecuzione:
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
Vedi anche: gperftools - file di profilo non scaricato
Il modo migliore per visualizzare questi dati che ho trovato finora è rendere l'output di pprof lo stesso formato che kcachegrind utilizza come input (sì, lo strumento Valgrind-project-viewer) e usare kcachegrind per visualizzare che:
google-pprof --callgrind main.out prof.out > callgrind.out
kcachegrind callgrind.out
Dopo l'esecuzione con uno di questi metodi, otteniamo un prof.out
file di dati del profilo come output. Possiamo visualizzare graficamente quel file come SVG con:
google-pprof --web main.out prof.out
che fornisce come un grafico di chiamata familiare come altri strumenti, ma con l'unità goffa di numero di campioni anziché secondi.
In alternativa, possiamo anche ottenere alcuni dati testuali con:
google-pprof --text main.out prof.out
che dà:
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
Vedi anche: Come utilizzare google perf tools
Testato in Ubuntu 18.04, gprof2dot 2019.11.30, valgrind 3.13.0, perf 4.15.18, kernel Linux 4.15.0, FLameGraph 1a0dc6985aad06e76857cf2a354bd5ba0c9ce96b, gperftools 2.5-2.