==================================================== ____ _ _ _ _ _ _ | _ \| | | | | | (_) | | | | | |_) | | __ _ ___| | _| | _ __ _| |__ | |_ | _ <| |/ _` |/ __| |/ / | | |/ _` | '_ \| __| | |_) | | (_| | (__| <| |____| | (_| | | | | |_ |____/|_|\__,_|\___|_|\_\______|_|\__, |_| |_|\__| _ __/ | |___/ ==================================================== Indice: 1. Cos'è il profiling 2. Compilare un programma con il supporto per il profiling 3. Opzioni di profiling 4, Esempio 4.1 Flat profile 4.2 Call graph == Cos'è il profiling == Il profiling è un'attività indispensabile per ogni programmatore Unix che a lavoro terminato deve passare alla fase di ottimizzazione del suo codice, cercando i colli di bottiglia, le fasi che richiedono un utilizzo maggiore delle risorse, tutte le chiamate a funzioni, eventuali bug ecc. Un profiler prende come input l'istanza di esecuzione del programma, e fornisce come output principalmente: * flat profile: informazioni su ogni funzione chiamata, quanto tempo il programma ha impiegato globalmente per eseguire quella data funzione e quante volte quella funzione è stata chiamata; * call graph: per ogni funzione, visualizza quale funzione l'ha invocata, quali funzioni a sua volta ha invocato, quante volte e quanto tempo è stato impiegato per eseguire ogni subroutine. == Compilare un programma con supporto per il profiling == Per il profiling prenderemo come punto di riferimento gprof, applicazione compresa nella suite GNU e praticamente un must per il profiling in ambiente Unix. Per fare in modo che un'applicazione supporti il profiling, e magari anche il debugging ottimizzato, va compilata con gcc -o file_exe src1.c src2.c ... srcn.c -g -pg L'opzione -g fornisce il supporto per il debugging ottimizzato con GDB, mentre invece -pg fornisce il supporto per il profiling con gprof. A questo punto, per creare i dati necessari per il profiling è necessario eseguire il programma e lasciarlo in esecuzione fino al suo termine. Alla fine di ciò sarà disponibile il file gmon.out, generato automaticamente dal profiler. Attenzione: per fare in modo che venga creato il file gmon.out, sul quale poi opererà il profiler, il programma deve terminare in modo pulito, ovvero tramite una exit o tramite il ritorno del main. Se il programma viene interrotto in altri modi (SIGINT, SIGTERM, SIGKILL) il file di profiling non verrà generato. Inoltre, il file gmon.out viene generato nella stessa directory dell'eseguibile, oppure nell'ultima directory di lavoro dell'eseguibile. Ovvero, se l'eseguibile al suo interno ha eseguito una chdir, il file gmon.out verrà scritto nell'ultima directory in cui il codice ha effettuato la chdir, e se su questa directory l'utente non ha i permessi di scrittura non verrà scritto affatto. Una volta che si ha il file gmon.out relativo all'istanza del programma eseguito, è possibile effettuare il profiling e statistiche su di esso attraverso gprof. == Opzioni di profiling == Vediamo le opzioni principali che mette a disposizione gprof: * -e : Attraverso questa opzione, la funzione specificata non verrà inclusa nelle statistiche di profiling. È possibile ovviamente specificare diverse funzioni da non includere, ognuna attraverso un -e diverso (es. -e foo1 -e foo2 ..) * -f : Si indica al profiler di fornire le statistiche relative solo alla funzione specificata. È possibile ovviamente specificare diverse funzioni da includere nel profiling, ognuna attraverso un -f diverso (-f foo1 -f foo2 ..) == Esempio == Consideriamo un esempio: un programma C++ che iteri su tutti i numeri da 2 a 1000, e ne stampi il fattoriale se e solo se il numero in questione è primo. Il programma è stato scritto con le librerie NTL per la gestione dei grandi numeri (il C++ non gestisce nativamente numeri con centinaia o migliaia di cifre): #include #include NTL_CLIENT // Verifica se un numero è primo (ritorna 1 in caso affermativo, 0 in caso contrario) bool prime (int n) { int i; if (n==2) return 1; if (n==3) return 1; if (!(n%2)) return 0; for (i=3; i <= (int) n/2; i+=2) if (!(n%i)) return 0; return 1; } // Calcola il fattoriale di un numero utilizzando la ricorsione ZZ fact (int n) { if (n==1) return to_ZZ(1); else return to_ZZ(n)*fact(n-1); } int main() { int i; for (i=2; i<=1000; i++) if (prime(i)) cout << i << " e' primo -> " << i << "! = " << fact(i) << endl; return 0; } e compiliamolo con il supporto per debugging e profiling: g++ -o test test.cpp -lntl -g -pg Volendo possiamo già avere un'idea dei tempi di esecuzione utilizzando l'utility Unix time: blacklight@nightmare:~$ time ./test > /dev/null real 0m0.182s user 0m0.176s sys 0m0.000s Effettuiamo ora il profiling attraverso gprof per vedere le statistiche di esecuzione del codice: gprof -b ./test Vedremo che l'output è nettamente diviso in due parti: * Flat profile * Call graph === Flat profile === Il flat profile ci fornisce informazioni sul tempo di esecuzione delle funzioni relativo al tempo totale di esecuzione (in percentuale), sul tempo cumulativo di esecuzione delle funzioni, sul numero di chiamate e sul tempo medio necessario per l'esecuzione di una data funzione: Flat profile: Each sample counts as 0.01 seconds. % cumulative self self total time seconds seconds calls Ts/call Ts/call name 66.67 0.06 0.06 zaddmul 11.11 0.07 0.01 NTL::PrintDigits(std::ostream&, long, long) 11.11 0.08 0.01 _ntl_zmul 11.11 0.09 0.01 _ntl_zsdiv 0.00 0.09 0.00 228045 0.00 0.00 NTL::ZZ::~ZZ() 0.00 0.09 0.00 76127 0.00 0.00 NTL::ZZ::ZZ(NTL::INIT_VAL_STRUCT const&, int) 0.00 0.09 0.00 76127 0.00 0.00 NTL::to_ZZ(int) 0.00 0.09 0.00 75959 0.00 0.00 NTL::ZZ::ZZ(NTL::ZZ&, NTL::INIT_TRANS_STRUCT const&) 0.00 0.09 0.00 75959 0.00 0.00 NTL::ZZ::ZZ() 0.00 0.09 0.00 75959 0.00 0.00 NTL::mul(NTL::ZZ&, NTL::ZZ const&, NTL::ZZ const&) 0.00 0.09 0.00 75959 0.00 0.00 NTL::operator*(NTL::ZZ const&, NTL::ZZ const&) 0.00 0.09 0.00 999 0.00 0.00 prime(int) 0.00 0.09 0.00 168 0.00 0.00 fact(int) 0.00 0.09 0.00 1 0.00 0.00 global constructors keyed to _Z5primei 0.00 0.09 0.00 1 0.00 0.00 __static_initialization_and_destruction_0(int, int) Visualizzando i vari campi della tabella: * time: Tempo relativo di esecuzione della funzione sul tempo totale di esecuzione del programma, espresso in percentuale * cumulative seconds: tempo di esecuzione totale della funzione, a cui si somma il tempo di esecuzione di tutte le funzioni al di sopra di essa * self seconds: tempo totale di esecuzione della funzione. La tabella è ordinata in base a questo campo * self Ts/call: tempo medio di esecuzione per ogni istanza della funzione * self Ts/call: tempo medio di esecuzione per ogni istanza della funzione, più il tempo di esecuzione di tutti i suoi figli === Call graph === Il call graph contiene l'elenco delle funzioni, con tempo relativo di esecuzione, tempo di esecuzione proprio e tempo di esecuzione dei figli, e l'elenco delle funzioni che la data funzione ha richiamato con il numero di volte in cui ognuna di esse è stata richiamata. Può infatti accadere che il freeze di un'applicazione, o il collo di bottiglia, non si verifichi direttamente in una funzione ma in una delle funzioni da essa richiamata. In questi casi, un esame del call graph aiuta a comprendere meglio la situazione. Ecco il call graph del nostro eseguibile: Call graph granularity: each sample hit covers 4 byte(s) for 11.11% of 0.09 seconds index % time self children called name [1] 66.7 0.06 0.00 zaddmul [1] ----------------------------------------------- [2] 11.1 0.01 0.00 NTL::PrintDigits(std::ostream&, long, long) [2] ----------------------------------------------- [3] 11.1 0.01 0.00 _ntl_zmul [3] ----------------------------------------------- [4] 11.1 0.01 0.00 _ntl_zsdiv [4] ----------------------------------------------- 0.00 0.00 168/228045 main [15] 0.00 0.00 75959/228045 NTL::operator*(NTL::ZZ const&, NTL::ZZ const&) [28] 0.00 0.00 151918/228045 fact(int) [30] [22] 0.0 0.00 0.00 228045 NTL::ZZ::~ZZ() [22] ----------------------------------------------- 0.00 0.00 76127/76127 NTL::to_ZZ(int) [24] [23] 0.0 0.00 0.00 76127 NTL::ZZ::ZZ(NTL::INIT_VAL_STRUCT const&, int) [23] ----------------------------------------------- 0.00 0.00 76127/76127 fact(int) [30] [24] 0.0 0.00 0.00 76127 NTL::to_ZZ(int) [24] 0.00 0.00 76127/76127 NTL::ZZ::ZZ(NTL::INIT_VAL_STRUCT const&, int) [23] ----------------------------------------------- 0.00 0.00 75959/75959 NTL::operator*(NTL::ZZ const&, NTL::ZZ const&) [28] [25] 0.0 0.00 0.00 75959 NTL::ZZ::ZZ(NTL::ZZ&, NTL::INIT_TRANS_STRUCT const&) [25] ----------------------------------------------- 0.00 0.00 75959/75959 NTL::operator*(NTL::ZZ const&, NTL::ZZ const&) [28] [26] 0.0 0.00 0.00 75959 NTL::ZZ::ZZ() [26] ----------------------------------------------- 0.00 0.00 75959/75959 NTL::operator*(NTL::ZZ const&, NTL::ZZ const&) [28] [27] 0.0 0.00 0.00 75959 NTL::mul(NTL::ZZ&, NTL::ZZ const&, NTL::ZZ const&) [27] ----------------------------------------------- 0.00 0.00 75959/75959 fact(int) [30] [28] 0.0 0.00 0.00 75959 NTL::operator*(NTL::ZZ const&, NTL::ZZ const&) [28] 0.00 0.00 75959/75959 NTL::ZZ::ZZ() [26] 0.00 0.00 75959/75959 NTL::mul(NTL::ZZ&, NTL::ZZ const&, NTL::ZZ const&) [27] 0.00 0.00 75959/75959 NTL::ZZ::ZZ(NTL::ZZ&, NTL::INIT_TRANS_STRUCT const&) [25] 0.00 0.00 75959/228045 NTL::ZZ::~ZZ() [22] ----------------------------------------------- 0.00 0.00 999/999 main [15] [29] 0.0 0.00 0.00 999 prime(int) [29] ----------------------------------------------- 75959 fact(int) [30] 0.00 0.00 168/168 main [15] [30] 0.0 0.00 0.00 168+75959 fact(int) [30] 0.00 0.00 151918/228045 NTL::ZZ::~ZZ() [22] 0.00 0.00 76127/76127 NTL::to_ZZ(int) [24] 0.00 0.00 75959/75959 NTL::operator*(NTL::ZZ const&, NTL::ZZ const&) [28] 75959 fact(int) [30] ----------------------------------------------- 0.00 0.00 1/1 __do_global_ctors_aux [111] [31] 0.0 0.00 0.00 1 global constructors keyed to _Z5primei [31] 0.00 0.00 1/1 __static_initialization_and_destruction_0(int, int) [32] ----------------------------------------------- 0.00 0.00 1/1 global constructors keyed to _Z5primei [31] [32] 0.0 0.00 0.00 1 __static_initialization_and_destruction_0(int, int) [32] ----------------------------------------------- Ogni funzione è identificata da un indice numerico univoco. Gli indici con i relativi nomi di funzione a cui si riferiscono sono relativi all'elenco ordinato del flat profile, e volendo c'è l'elenco delle coppie indice -> nome funzione riportato poco sotto: Index by function name [31] global constructors keyed to _Z5primei (test.cpp) [23] NTL::ZZ::ZZ(NTL::INIT_VAL_STRUCT const&, int) [24] NTL::to_ZZ(int) [32] __static_initialization_and_destruction_0(int, int) (test.cpp) [25] NTL::ZZ::ZZ(NTL::ZZ&, NTL::INIT_TRANS_STRUCT const&) [28] NTL::operator*(NTL::ZZ const&, NTL::ZZ const&) [30] fact(int) [26] NTL::ZZ::ZZ() [3] _ntl_zmul [29] prime(int) [22] NTL::ZZ::~ZZ() [4] _ntl_zsdiv [2] NTL::PrintDigits(std::ostream&, long, long) [27] NTL::mul(NTL::ZZ&, NTL::ZZ const&, NTL::ZZ const&) [1] zaddmul