Come posso profilare il codice C ++ in esecuzione su Linux?

Ho un’applicazione C ++, in esecuzione su Linux, che sono in fase di ottimizzazione. Come posso individuare quali aree del mio codice funzionano lentamente?

Se il tuo objective è usare un profiler, usa uno di quelli suggeriti.

Tuttavia, se sei di fretta e puoi interrompere manualmente il tuo programma sotto il debugger mentre è soggettivamente lento, c’è un modo semplice per trovare problemi di prestazioni.

Basta fermarlo più volte e ogni volta guarda lo stack delle chiamate. Se c’è del codice che sta sprecando una certa percentuale del tempo, il 20% o il 50% o qualsiasi altra cosa, è la probabilità che lo acchiappi nell’atto su ciascun campione. Quindi questa è approssimativamente la percentuale di campioni su cui la vedrai. Non c’è bisogno di congetture istruite. Se hai una supposizione su quale sia il problema, questo lo dimostrerà o lo smentirà.

Potresti avere più problemi di prestazioni di dimensioni diverse. Se si pulisce uno di essi, i rimanenti prenderanno una percentuale maggiore e saranno più facili da individuare nei passaggi successivi. Questo effetto di ingrandimento , se combinato a più problemi, può portare a fattori di accelerazione davvero massicci.

Avvertenza: i programmatori tendono a essere scettici su questa tecnica a meno che non l’abbiano usata loro stessi. Diranno che i profiler ti forniscono queste informazioni, ma ciò è vero soltanto se campionano l’intero stack di chiamate e poi ti permettono di esaminare un insieme casuale di campioni. (I sumri sono dove l’intuizione è persa.) I grafici delle chiamate non ti danno le stesse informazioni, perché

  1. non riassumono al livello di istruzione, e
  2. danno riassunti confusi in presenza di ricorsione.

Diranno anche che funziona solo su programmi giocattolo, quando in realtà funziona su qualsiasi programma, e sembra funzionare meglio su programmi più grandi, perché tendono ad avere più problemi da trovare. Diranno che a volte trova cose che non sono problemi, ma è vero solo se vedi qualcosa una volta . Se vedi un problema su più di un campione, è reale.

PS Questo può essere fatto anche su programmi multi-thread se esiste un modo per raccogliere campioni di stack di chiamate del pool di thread in un momento specifico, come in Java.

PPS Come generalità approssimativa, più livelli di astrazione si hanno nel software, più è probabile che si trovi che questa è la causa dei problemi di prestazioni (e l’opportunità di ottenere l’aumento di velocità).

Aggiunto: Potrebbe non essere ovvio, ma la tecnica di campionamento dello stack funziona altrettanto bene in presenza di ricorsione. Il motivo è che il tempo che verrebbe risparmiato eliminando un’istruzione viene approssimato dalla frazione di campioni che lo contengono, indipendentemente dal numero di volte in cui può verificarsi all’interno di un campione.

Un’altra obiezione che sento spesso è: ” Si fermerà in un posto casuale, e perderà il vero problema “. Questo deriva dall’avere un precedente concetto di quale sia il vero problema. Una proprietà chiave dei problemi di prestazioni è che sfidano le aspettative. Il campionamento ti dice che qualcosa è un problema e la tua prima reazione è l’incredulità. È naturale, ma puoi essere sicuro che se trova un problema è reale, e viceversa.

AGGIUNTO: permettimi di fare una spiegazione bayesiana su come funziona. Supponiamo che ci sia qualche istruzione I (chiamata o altro) che è nello stack delle chiamate una frazione f del tempo (e quindi costa così tanto). Per semplicità, supponiamo di non sapere cosa sia f , ma supponiamo che sia 0.1, 0.2, 0.3, … 0.9, 1.0, e la probabilità precedente di ognuna di queste possibilità sia 0.1, quindi tutti questi costi sono uguali probabilmente a priori.

Quindi supponiamo di prendere solo 2 campioni di stack e vediamo l’istruzione I su entrambi i campioni, l’osservazione designata o=2/2 . Questo ci dà nuove stime della frequenza f di I , secondo questo:

 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) 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 

L’ultima colonna dice che, ad esempio, la probabilità che f > = 0,5 sia del 92%, rispetto all’assunzione precedente del 60%.

Supponiamo che le ipotesi precedenti siano diverse. Supponiamo di supporre che P (f = 0,1) sia pari a .991 (quasi certo) e che tutte le altre possibilità siano quasi impossibili (0,001). In altre parole, la nostra precedente certezza è che I sono a buon mercato. Quindi otteniamo:

 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) 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 

Ora dice che P (f> = 0,5) è del 26%, in aumento rispetto all’assunzione precedente dello 0,6%. Quindi Bayes ci consente di aggiornare la nostra stima del probabile costo di I Se la quantità di dati è piccola, non ci dice esattamente quale sia il costo, ma solo che è abbastanza grande da valerne la pena.

Un altro modo di guardare è chiamato la regola di successione . Se si lancia una moneta 2 volte, e se esce testa entrambe le volte, cosa ti dice della probabile ponderazione della moneta? Il modo rispettato di rispondere è di dire che è una distribuzione Beta, con valore medio (numero di risultati + 1) / (numero di tentativi + 2) = (2 + 1) / (2 + 2) = 75%.

(La chiave è che ci vediamo più di una volta. Se la vediamo solo una volta, questo non ci dice molto eccetto che f > 0.)

Quindi, anche un numero molto piccolo di campioni può dirci molto sul costo delle istruzioni che vede. (E li vedrà con una frequenza, in media, proporzionale al loro costo.Se sono prelevati n campioni, e f è il costo, allora apparirò su nf+/-sqrt(nf(1-f)) esempi. , n=10 , f=0.3 , cioè 3+/-1.4 campioni.)


AGGIUNTO, per dare un touch intuitivo alla differenza tra la misurazione e il campionamento casuale della pila:
Ora ci sono profilatori che campionano lo stack, anche a tempo di orologio da parete, ma ciò che viene fuori è misurazioni (o percorso caldo, o punto caldo, da cui un “collo di bottiglia” può facilmente nascondersi). Quello che non ti mostrano (e potrebbero facilmente farlo) sono gli stessi campioni stessi. E se il tuo objective è trovare il collo di bottiglia, il numero di loro che devi vedere è, in media , 2 diviso per la frazione di tempo che impiega. Quindi, se impiega il 30% del tempo, in media 2 / .3 = 6,7 campioni lo mostrerà e la probabilità che 20 campioni lo mostrino è del 99,2%.

Ecco un’illustrazione “a arm” della differenza tra l’esame delle misurazioni e l’esame dei campioni di pila. Il collo di bottiglia potrebbe essere un grosso blob come questo, o numerosi piccoli, non fa differenza.

inserisci la descrizione dell'immagine qui

La misurazione è orizzontale; ti dice quale frazione di tempo hanno le routine specifiche. Il campionamento è verticale. Se c’è un modo per evitare ciò che l’intero programma sta facendo in quel momento, e se lo vedi su un secondo campione , hai trovato il collo di bottiglia. Questo è ciò che fa la differenza: vedere l’intera ragione del tempo trascorso, non solo quanto.

Puoi usare Valgrind con le seguenti opzioni

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

callgrind.out.x un file chiamato callgrind.out.x . È quindi ansible utilizzare kcachegrind strumento kcachegrind per leggere questo file. Ti darà un’analisi grafica delle cose con risultati come quali linee costano quanto.

Presumo che tu stia usando GCC. La soluzione standard sarebbe il profilo con gprof .

Assicurati di aggiungere -pg alla compilazione prima di profilare:

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

Non l’ho ancora provato, ma ho sentito cose positive su google-perftools . Vale sicuramente la pena provare.

Domanda correlata qui

Qualche altra parola d’ordine se gprof non fa il lavoro per te: Valgrind , Intel VTune , Sun DTrace .

I kernel più recenti (ad esempio gli ultimi kernel di Ubuntu) vengono forniti con i nuovi strumenti “perf” ( apt-get install linux-tools ) AKA perf_events .

Questi sono dotati di classici profilatori di campionamento ( man-page ) e del fantastico diagramma di marcia !

L’importante è che questi strumenti possano essere profili di sistema e non solo profili di processo: possono mostrare l’interazione tra thread, processi e kernel e consentono di comprendere la pianificazione e le dipendenze I / O tra i processi.

Alt text

Userei Valgrind e Callgrind come base per la mia suite di strumenti di profilazione. Quello che è importante sapere è che Valgrind è fondamentalmente una macchina virtuale:

(wikipedia) Valgrind è essenzialmente una macchina virtuale che utilizza tecniche di compilazione just-in-time (JIT), inclusa la ricompilazione dynamic. Nulla del programma originale viene mai eseguito direttamente sul processore host. Invece, Valgrind prima traduce il programma in una forma temporanea, più semplice, chiamata Intermediate Representation (IR), che è una forma basata su processore e basata su SSA. Dopo la conversione, uno strumento (vedi sotto) è libero di fare le trasformazioni che vorrebbe sull’IR, prima che Valgrind traduca l’IR in codice macchina e faccia funzionare il processore host.

Callgrind è un profiler costruito su questo. Il vantaggio principale è che non è necessario eseguire l’applicazione per ore per ottenere risultati affidabili. Anche una sola esecuzione è sufficiente per ottenere risultati affidabili, perché Callgrind è un profiler che non rileva.

Un altro strumento costruito su Valgrind è Massif. Lo uso per profilare l’utilizzo della memoria heap. Funziona alla grande. Quello che fa è che ti dà istantanee dell’uso della memoria – informazioni dettagliate che cosa detiene quale percentuale di memoria, e l’OMS l’aveva messo lì. Tali informazioni sono disponibili in diversi momentjs di esecuzione dell’applicazione.

Questa è una risposta alla risposta Gprof di Nazgob .

Ho usato Gprof negli ultimi due giorni e ho già trovato tre limiti significativi, uno dei quali non ho ancora visto nessun documento (ancora):

  1. Non funziona correttamente sul codice multi-thread, a meno che non si utilizzi una soluzione alternativa

  2. Il grafico delle chiamate viene confuso dai puntatori di funzione. Esempio: ho una funzione chiamata multithread () che mi abilita a multithreading di una funzione specificata su un array specificato (entrambi passati come argomenti). Tuttavia, Gprof visualizza tutte le chiamate a multithread () come equivalenti ai fini del calcolo del tempo trascorso nei bambini. Dal momento che alcune funzioni che passo al multithread () richiedono molto più tempo di altre, i miei grafici delle chiamate sono per lo più inutili. (A chi si chiede se il threading sia il problema qui: no, multithread () può opzionalmente, e fatto in questo caso, eseguire tutto in modo sequenziale solo sul thread chiamante).

  3. Qui dice che “… le cifre sul numero di chiamate sono ricavate contando, non campionando, sono completamente accurate …”. Tuttavia trovo che il mio grafo delle chiamate mi dia 5345859132 + 784984078 come statistiche di chiamata alla mia funzione più chiamata, dove il primo numero dovrebbe essere chiamate dirette e le seconde chiamate ricorsive (che sono tutte provenienti da se stesse). Poiché ciò implicava un errore, ho inserito dei contatori lunghi (64 bit) nel codice e ho eseguito nuovamente la stessa operazione. Il mio conteggio: 5345859132 diretto e 78094395406 chiamate ricorsive. Ci sono molte cifre lì, quindi sottolineo che le chiamate ricorsive che ho misurato sono 78bn, contro 784m da Gprof: un fattore di 100 differenti. Entrambe le esecuzioni erano codice a thread singolo e non ottimizzato, uno compilato -g e l’altro -pg.

Questo era GNU Gprof (GNU Binutils per Debian) 2.18.0.20080103 in esecuzione con Debian Lenny a 64 bit, se questo aiuta qualcuno.

La risposta per eseguire valgrind --tool=callgrind non è del tutto completa senza alcune opzioni. Di solito non vogliamo profilare 10 minuti di tempo di avvio lento con Valgrind e vogliamo profilare il nostro programma quando sta svolgendo qualche compito.

Quindi questo è quello che raccomando. Esegui prima il programma:

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

Ora quando funziona e vogliamo iniziare la profilazione, dovremmo eseguire in un’altra finestra:

 callgrind_control -i on 

Questo triggers la profilazione. Per distriggersrlo e interrompere l’intera attività, potremmo utilizzare:

 callgrind_control -k 

Ora abbiamo alcuni file chiamati callgrind.out. * Nella directory corrente. Per vedere i risultati del profilo utilizzare:

 kcachegrind callgrind.out.* 

Raccomando nella prossima finestra di cliccare sull’intestazione di colonna “Self”, altrimenti mostra che “main ()” è un’attività che richiede molto tempo. “Self” mostra quanto ogni funzione stessa abbia richiesto del tempo, non insieme ai dipendenti.

Usa Valgrind, callgrind e kcachegrind:

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

genera callgrind.out.x. Leggilo usando kcachegrind.

Usa gprof (aggiungi -pg):

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

(non così buono per multi-thread, puntatori di funzioni)

Utilizza google-perftools:

Utilizza il campionamento temporale, rivela I / O e i colli di bottiglia della CPU vengono rivelati.

Intel VTune è il migliore (gratuito per scopi didattici).

Altri: AMD Codeanalyst, OProfile, strumenti ‘perf’ (apt-get install linux-tools)

Per i programmi a thread singolo è ansible utilizzare igprof , The Ignominous Profiler: https://igprof.org/ .

È un profiler di campionamento, sulla falsariga della … lunga … risposta di Mike Dunlavey, che conterrà i risultati in uno stack stack di chiamate sfogliabile, annotato con il tempo o la memoria spesi in ciascuna funzione, sia cumulativo sia per-funzione.

Questi sono i due metodi che utilizzo per velocizzare il mio codice:

Per le applicazioni con vincolo CPU:

  1. Utilizza un profiler in modalità DEBUG per identificare parti discutibili del tuo codice
  2. Quindi passa alla modalità RELEASE e commenta le sezioni discutibili del tuo codice (mozzala con niente) finché non vedi le modifiche nelle prestazioni.

Per le applicazioni con collegamento I / O:

  1. Usa un profiler in modalità RELEASE per identificare parti discutibili del tuo codice.

NB

Se non hai un profiler, usa il profiler del pover’uomo. Premi pausa durante il debug dell’applicazione. La maggior parte delle suite per sviluppatori si romperà in assembly con numeri di riga commentati. Probabilmente statisticamente atterrerai in una regione che sta mangiando la maggior parte dei tuoi cicli di CPU.

Per la CPU, il motivo della profilazione nella modalità DEBUG è dovuto al fatto che se si tenta la profilazione in modalità RELEASE , il compilatore ridurrà i calcoli matematici, i cicli vettoriali e le funzioni inline che tendono a raggruppare il codice in un disordine non mappabile al momento dell’assemblaggio. Un disordine non mappabile significa che il tuo profiler non sarà in grado di identificare chiaramente ciò che sta richiedendo così tanto tempo perché l’assembly potrebbe non corrispondere al codice sorgente in ottimizzazione . Se è necessaria la prestazione (ad es. Sincronizzazione temporale) della modalità RELEASE , disabilitare le funzionalità del debugger secondo necessità per mantenere una prestazione utilizzabile.

Per il bounding I / O, il profiler può comunque identificare le operazioni di I / O in modalità RELEASE perché le operazioni di I / O sono collegate esternamente a una libreria condivisa (il più delle volte) o nel peggiore dei casi, risulteranno in un sistema call interrupt vector (che è anche facilmente identificabile dal profiler).