profile e pstats - Analisi delle Prestazioni

Scopo: Analisi delle prestazioni dei programmi Python

Il modulo profile fornisce le API per collezionare e analizzare dati statistici sul modo nel quale il sorgente Pyhton utilizza le risorse del processore.

Il risultato dell'esecuzione della profilazione può variare a seconda dell'equipaggiamento della macchina nella quale vengono eseguiti gli esempi.

Eseguire il Profilatore

Il punto più basico da cui partire nel modulo profile è run(). Riceve una singola istruzione come argomento, e crea un report con il tempo trascorso per l'esecuzione delle varie righe di codice mentre viene eseguita l'istruzione.

# profile_fibonacci_raw.py

import profile


def fib(n):
    # da literateprograms.org
    # http://bit.ly/hlOQ5m
    if n == 0:
        return 0
    elif n == 1:
        return 1
    else:
        return fib(n - 1) + fib(n - 2)


def fib_seq(n):
    seq = []
    if n > 0:
        seq.extend(fib_seq(n - 1))
    seq.append(fib(n))
    return seq


profile.run('print(fib_seq(20)); print()')

Questa funzione ricorsiva per un calcolatore di sequenze di Fibonacci è particolarmente utile per la dimostrazione di profile visto che le prestazioni possono essere migliorate in modo significativo. Il formato standard del report mostra un sommario seguito dai dettagli per ciascuna funzione eseguita.

$ python3 profile_fibonacci_raw.py

[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765]

         57359 function calls (69 primitive calls) in 0.128 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
       21    0.000    0.000    0.000    0.000 :0(append)
        1    0.000    0.000    0.127    0.127 :0(exec)
       20    0.000    0.000    0.000    0.000 :0(extend)
        2    0.000    0.000    0.000    0.000 :0(print)
        1    0.001    0.001    0.001    0.001 :0(setprofile)
        1    0.000    0.000    0.127    0.127 <string>:1(<module>)
        1    0.000    0.000    0.128    0.128 profile:0(print(fib_seq(20)); print())
        0    0.000             0.000          profile:0(profiler)
     21/1    0.000    0.000    0.127    0.127 profile_fibonacci_raw.py:17(fib_seq)
 57291/21    0.126    0.000    0.126    0.006 profile_fibonacci_raw.py:6(fib)


La versione grezza richiede 57359 chiamate di funzione separate e impiega per l'esecuzione più o meno 0.100 secondi. Il fatto che si siano solo 69 chiamate primitive dice che la vasta maggioranza delle 57 mila e passa chiamate sono ricorsive. Il dettaglio sul dove il tempo è stato speso è per funzione nel listato che mostra il numero di chiamate, il tempo totale trascorso nella funzione, il tempo per chiamata (tottime/ncalls), il tempo cumulativo trascorso nella funzione e il rapporto tra tempo cumulativo e chiamate primitive.

Meno che sorprendentemente, la maggior parte del tempo viene speso chiamando fib() ripetutamente. L'aggiunta di un decoratore di cache riduce il numero di chiamate ricorsive, ed ha un grosso impatto sulle prestazioni di questa funzione.

# profile_fibonacci_memoized.py

import functools
import profile


@functools.lru_cache(maxsize=None)
def fib(n):
    # da literateprograms.org
    # http://bit.ly/hlOQ5m
    if n == 0:
        return 0
    elif n == 1:
        return 1
    else:
        return fib(n - 1) + fib(n - 2)


def fib_seq(n):
    seq = []
    if n > 0:
        seq.extend(fib_seq(n - 1))
    seq.append(fib(n))
    return seq


if __name__ == '__main__':
    profile.run('print(fib_seq(20)); print()')

Trattenendo in memoria il valore di Fibonacci a ogni livello, la maggior parte della ricorsione viene evitata e l'esecuzione si riduce ad 89 chiamate che richiedono solo 0.001 secondi. Il conteggio di ncalls per fib() mostra che non viene mai chiamata ricorsivamente.

$ python3 profile_fibonacci_memoized.py

[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765]

         89 function calls (69 primitive calls) in 0.001 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
       21    0.000    0.000    0.000    0.000 :0(append)
        1    0.000    0.000    0.000    0.000 :0(exec)
       20    0.000    0.000    0.000    0.000 :0(extend)
        2    0.000    0.000    0.000    0.000 :0(print)
        1    0.001    0.001    0.001    0.001 :0(setprofile)
        1    0.000    0.000    0.000    0.000 <string>:1(<module>)
        1    0.000    0.000    0.001    0.001 profile:0(print(fib_seq(20)); print())
        0    0.000             0.000          profile:0(profiler)
     21/1    0.000    0.000    0.000    0.000 profile_fibonacci_memoized.py:19(fib_seq)
       21    0.000    0.000    0.000    0.000 profile_fibonacci_memoized.py:7(fib)


Esecuzione in un Contesto

Talvolta, invece che costruire una espressione complessa per run(), è più facile costruire una espressione semplice e passarle parametri tramite un contesto, usando runctx().

# profile_runctx.py

import profile
from profile_fibonacci_memoized import fib, fib_seq

if __name__ == '__main__':
    profile.runctx(
        'print(fib_seq(n)); print()',
        globals(),
        {'n': 20},
    )

In questo esempio, il valore di n è passato tramite la variabile locale di contesto invece che essere direttamente incorporata nell'istruzione passata a runctx().

$ python3 profile_runctx.py

[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765]

         89 function calls (69 primitive calls) in 0.001 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
       21    0.000    0.000    0.000    0.000 :0(append)
        1    0.000    0.000    0.000    0.000 :0(exec)
       20    0.000    0.000    0.000    0.000 :0(extend)
        2    0.000    0.000    0.000    0.000 :0(print)
        1    0.001    0.001    0.001    0.001 :0(setprofile)
        1    0.000    0.000    0.000    0.000 <string>:1(<module>)
        1    0.000    0.000    0.001    0.001 profile:0(print(fib_seq(n)); print())
        0    0.000             0.000          profile:0(profiler)
     21/1    0.000    0.000    0.000    0.000 profile_fibonacci_memoized.py:19(fib_seq)
       21    0.000    0.000    0.000    0.000 profile_fibonacci_memoized.py:7(fib)


pstats: Salvare e Lavorare con le Statistiche

Il report standard creato dalle funzioni in profile non è molto flessibile. Possono comunque essere prodotti report personalizzati salvato la profilatura grezza dei dati in uscita da run() e runctx(), quindi elaborandoli separatamente con la classe pstats.Stats.

Questo esempio esegue parecchie iterazioni per lo stesso test e combina i risultati.

# profile_stats.py

import cProfile as profile
import pstats
from profile_fibonacci_memoized import fib, fib_seq

# Crea 5 insiemi di statistiche
for i in range(5):
    filename = 'profile_stats_{}.stats'.format(i)
    profile.run('print({}, fib_seq(20))'.format(i), filename)

# Legge i 5 file di statistica in un singolo oggetto
stats = pstats.Stats('profile_stats_0.stats')
for i in range(1, 5):
    stats.add('profile_stats_{}.stats'.format(i))

# Toglie i nomi delle directory dai file per il report
stats.strip_dirs()

# Ordina le statistiche per tempo cumulativo trascorso
# nella funzione
stats.sort_stats('cumulative')

stats.print_stats()

Il report in uscita è in ordine discendente per tempo cumulativo trascorso nella funzione e i nomi di directory sono rimossi dai nomi dei file stampati per risparmiare spazio orizzontale nella pagina.

$ python3 profile_stats.py

0 [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765]
1 [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765]
2 [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765]
3 [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765]
4 [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765]
Thu Jun  3 09:15:32 2021    profile_stats_0.stats
Thu Jun  3 09:15:32 2021    profile_stats_1.stats
Thu Jun  3 09:15:32 2021    profile_stats_2.stats
Thu Jun  3 09:15:32 2021    profile_stats_3.stats
Thu Jun  3 09:15:32 2021    profile_stats_4.stats

         351 function calls (251 primitive calls) in 0.001 seconds

   Ordered by: cumulative time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        5    0.000    0.000    0.001    0.000 {built-in method builtins.exec}
        5    0.000    0.000    0.000    0.000 <string>:1(<module>)
    105/5    0.000    0.000    0.000    0.000 profile_fibonacci_memoized.py:19(fib_seq)
        5    0.000    0.000    0.000    0.000 {built-in method builtins.print}
      100    0.000    0.000    0.000    0.000 {method 'extend' of 'list' objects}
      105    0.000    0.000    0.000    0.000 {method 'append' of 'list' objects}
       21    0.000    0.000    0.000    0.000 profile_fibonacci_memoized.py:7(fib)
        5    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}


Limitare il Contenuto del Report

Il risultato può essere circoscritto per funzione. Questa versione mostra solo le informazioni circa le prestazioni per fib() e fib_seq() usando una espressione regolare per trovare corrispondenza con i valori desiderati per filename:lineno(function).

# profile_stats_restricted.py

import profile
import pstats
from profile_fibonacci_memoized import fib, fib_seq

# Legge tutti e 5 i file di statistica in un singolo oggetto
stats = pstats.Stats('profile_stats_0.stats')
for i in range(1, 5):
    stats.add('profile_stats_{}.stats'.format(i))
stats.strip_dirs()
stats.sort_stats('cumulative')

# limita l'output alle righe che contengono  "(fib"
stats.print_stats('\(fib')

L'espressione regolare include una parentesi sinistra letterale (() per trovare corrispondenza con la porzione del nome di funzione del valore.

$ python3 profile_stats_restricted.py

Thu Jun  3 09:15:32 2021    profile_stats_0.stats
Thu Jun  3 09:15:32 2021    profile_stats_1.stats
Thu Jun  3 09:15:32 2021    profile_stats_2.stats
Thu Jun  3 09:15:32 2021    profile_stats_3.stats
Thu Jun  3 09:15:32 2021    profile_stats_4.stats

         351 function calls (251 primitive calls) in 0.001 seconds

   Ordered by: cumulative time
   List reduced from 8 to 2 due to restriction <'\\(fib'>

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
    105/5    0.000    0.000    0.000    0.000 profile_fibonacci_memoized.py:19(fib_seq)
       21    0.000    0.000    0.000    0.000 profile_fibonacci_memoized.py:7(fib)


Grafi di Chiamante e Chiamato

Stats include anche metodi per stampare i chiamanti e le chiamate di funzioni.

# profile_stats_callers.py

import cProfile as profile
import pstats
from profile_fibonacci_memoized import fib, fib_seq

# Legge tutti e 5 i file di statistica in un singolo oggetto
stats = pstats.Stats('profile_stats_0.stats')
for i in range(1, 5):
    stats.add('profile_stats_{}.stats'.format(i))
stats.strip_dirs()
stats.sort_stats('cumulative')

print('CHIAMANTI IN ENTRATA:')
stats.print_callers('\(fib')

print('CHIAMATE IN USCITA  :')
stats.print_callees('\(fib')

Gli argomenti per print_callers() e print_callees() funzionano allo stesso modo degli argomenti di restrizione per print_stats(). Il risultato mostra il chiamante, il chiamato, il numero di chiamate e il tempo cumulativo.

$ python3 profile_stats_callers.py

CHIAMANTI IN ENTRATA:
   Ordered by: cumulative time
   List reduced from 8 to 2 due to restriction <'\\(fib'>

Function                                   was called by...
                                               ncalls  tottime  cumtime
profile_fibonacci_memoized.py:19(fib_seq)  <-       5    0.000    0.000  <string>:1(<module>)
                                                100/5    0.000    0.000  profile_fibonacci_memoized.py:19(fib_seq)
profile_fibonacci_memoized.py:7(fib)       <-      21    0.000    0.000  profile_fibonacci_memoized.py:19(fib_seq)


CHIAMATE IN USCITA  :
   Ordered by: cumulative time
   List reduced from 8 to 2 due to restriction <'\\(fib'>

Function                                   called...
                                               ncalls  tottime  cumtime
profile_fibonacci_memoized.py:19(fib_seq)  ->      21    0.000    0.000  profile_fibonacci_memoized.py:7(fib)
                                                100/5    0.000    0.000  profile_fibonacci_memoized.py:19(fib_seq)
                                                  105    0.000    0.000  {method 'append' of 'list' objects}
                                                  100    0.000    0.000  {method 'extend' of 'list' objects}
profile_fibonacci_memoized.py:7(fib)       -> 


.

Vedere anche:

profile
La documentazione della libreria standard per questo modulo.
functools.lru-cache()
Il decoratore cache usato per migliorare le prestazioni in questi esempi
The Stats Class
La documentazione della libreria standard per pstats.Stats
Gprof2Dot
Strumento di visualizzazione dei dati forniti da profile
Smiley
Tracciatore per Applicazioni Python