PyMOTW: profile, cProfile, pstats

3 Septiembre 2008

Traducción de PyMOTW: profile, cProfile, pstats los módulos profile y cProfilede la columna semanal de Doug Hellmann.


Comentarios

Los módulos profile y cProfile proporcionan interfaces de programación para coleccionar y analizar estadísticas sobre cómo código Python consume recursos de procesamiento.

Módulo: profile
Propósito: Análisis de de rendimiento de programas en Python
Versiones de Python: 1.4 y posterior, estos ejemplos son para Python 2.5

run():

El punto más básico de inicio en el módulo profile es run(). Acepta una cadena como argumento, y crea un reporte del tiempo empleado ejecutando diversas líneas de código mientras ejecuta la instrucción.

import profile

def fib(n):
    # de http://en.literateprograms.org/Fibonacci_numbers_(Python)
    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

print 'RUDIMENTARIO'
print '=' * 80
profile.run('print fib_seq(20); print')

Esta versión recursiva de un calculador de una secuencia Fibonacci es especialmente útil para demostrar el módulo profile porque podemos tanto mejorar el rendimiento. El formato estándar del reporte muestra un sumario y detalles de cada función ejecutada.

$ python profile_fibonacci_raw.py
RUDIMENTARIO
================================================================================
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765]

         57356 function calls (66 primitive calls) in 0.584 CPU seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
       21    0.000    0.000    0.000    0.000 :0(append)
       20    0.000    0.000    0.000    0.000 :0(extend)
        1    0.000    0.000    0.000    0.000 :0(setprofile)
        1    0.000    0.000    0.580    0.580 <string>:1(<module>)
        1    0.004    0.004    0.584    0.584 profile:0(print fib_seq(20); print)
        0    0.000             0.000          profile:0(profiler)
 57291/21    0.580    0.000    0.580    0.028 profile_fibonacci_raw.py:13(fib)
     21/1    0.000    0.000    0.580    0.580 profile_fibonacci_raw.py:22(fib_seq)

Como puedes ver, la ejecución requiere de 57356 invocaciones de función y toma 1/2 segundo. Ya que hay tan solo 66 invocaciones primitivas, sabemos que la gran mayoría de las 57 mil invocaciones fueron recursivas. Los detalles sobre donde se pasó el tiempo están separados por función en el listado que muestra el numero de invocaciones, el tiempo total transcurrido en la función, el tiempo por invocación (tottime/ncalls), tiempo acumulado transcurrido en una función y la relación de tiempo acumulado a invocaciones primitivas.

No es sorprendente que la mayoría del tiempo aquí transcurre invocando fib() en repetidas ocasiones. Podemos añadir un decorador memoize para reducir el número de invocaciones recursivas y obtener un impacto importante en el rendimiento de esta función.

import profile

class memoize:
    # de http://avinashv.net/2008/04/python-decorators-syntactic-sugar/
    def __init__(self, function):
        self.function = function
        self.memoized = {}

    def __call__(self, *args):
        try:
            return self.memoized[args]
        except KeyError:
            self.memoized[args] = self.function(*args)
            return self.memoized[args]

@memoize
def fib(n):
    # de http://en.literateprograms.org/Fibonacci_numbers_(Python)
    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__':
    print 'MEMOIZADO'
    print '=' * 80
    profile.run('print fib_seq(20); print')

Recordando el valor de (la serie) Fibonacci podemos evitar la mayoría de la recursión y reducir a 145 las invocaciones que tan sólo toman 0.008 segundos. También nota que el contador ncalls para fib() muestra que nunca entra en recursión.

$ python profile_fibonacci_memoized.py
MEMOIZADO
================================================================================
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765]

         145 function calls (87 primitive calls) in 0.008 CPU seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
       21    0.000    0.000    0.000    0.000 :0(append)
       20    0.000    0.000    0.000    0.000 :0(extend)
        1    0.004    0.004    0.004    0.004 :0(setprofile)
        1    0.000    0.000    0.004    0.004 <string>:1(<module>)
        1    0.000    0.000    0.008    0.008 profile:0(print fib_seq(20); print)
        0    0.000             0.000          profile:0(profiler)
    59/21    0.000    0.000    0.000    0.000 profile_fibonacci_memoized.py:19(__call__)
       21    0.000    0.000    0.000    0.000 profile_fibonacci_memoized.py:26(fib)
     21/1    0.004    0.000    0.004    0.004 profile_fibonacci_memoized.py:36(fib_seq)

runctx():

Obviamente, no siempre es fácil construir la expresión para pasar a run(). Algunas veces es más fácil construir una expresión simple y ejecutarla en un contexto con (variables) globales y locales, usando runctx().

import profile
from profile_fibonacci_memoized import fib, fib_seq

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

En este ejemplo, el valor de "n" es pasado mediante el contexto de variables locales en lugar de estar embebido directamente en la instrucción que se pasa a runctx().

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

         145 function calls (87 primitive calls) in 0.004 CPU seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
       21    0.000    0.000    0.000    0.000 :0(append)
       20    0.000    0.000    0.000    0.000 :0(extend)
        1    0.000    0.000    0.000    0.000 :0(setprofile)
        1    0.000    0.000    0.004    0.004 <string>:1(<module>)
        1    0.000    0.000    0.004    0.004 profile:0(print fib_seq(n); print)
        0    0.000             0.000          profile:0(profiler)
    59/21    0.000    0.000    0.004    0.000 profile_fibonacci_memoized.py:19(__call__)
       21    0.004    0.000    0.004    0.000 profile_fibonacci_memoized.py:26(fib)
     21/1    0.000    0.000    0.004    0.004 profile_fibonacci_memoized.py:36(fib_seq)

pstats: Guardando y trabajando con estadíticas:

Si el reporte estándar no está formateado de la manera que lo necesitas, ambos run() y runctx() aceptan un nombre de archivo como argumento para guardar los datos en un archivo en lugar de mostrar el reporte. La clase Stats del módulo pstats sabe cómo leer el archivo y puede ser usada para manipular los datos.

Por ejemplo, para ejecutar varias veces el mismo test y combinar los resultados, puede hacer algo como esto:

import profile
import pstats
from profile_fibonacci_memoized import fib, fib_seq

# Crea 5 conjuntos de estadística
filenames = []
for i in range(5):
    filename = 'profile_stats_%d.stats' % i
    profile.run('print %d, fib_seq(20)' % i, filename)

# Lee los 5 archivos con estadísticas en un solo objeto 
stats = pstats.Stats('profile_stats_0.stats')
for i in range(1, 5):
    stats.add('profile_stats_%d.stats' % i)

# Limpia nombres de archivos para el reporte
stats.strip_dirs()

# Ordena las estadísticas por el tiempo acumulado en la función
stats.sort_stats('cumulative')

stats.print_stats()

El reporte resultante está ordenado en orden descendiente de tiempo acumulado transcurrido en la función y los nombres de directorios son eliminados de los archivos mostrados para ahorra espacio horizontal.

$ python 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]
Wed Sep  3 21:22:27 2008    profile_stats_0.stats
Wed Sep  3 21:22:27 2008    profile_stats_1.stats
Wed Sep  3 21:22:27 2008    profile_stats_2.stats
Wed Sep  3 21:22:27 2008    profile_stats_3.stats
Wed Sep  3 21:22:27 2008    profile_stats_4.stats

         489 function calls (351 primitive calls) in 0.032 CPU seconds

   Ordered by: cumulative time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        5    0.004    0.001    0.024    0.005 <string>:1(<module>)
    105/5    0.012    0.000    0.020    0.004 profile_fibonacci_memoized.py:36(fib_seq)
        1    0.000    0.000    0.012    0.012 profile:0(print 0, fib_seq(20))
  143/105    0.004    0.000    0.008    0.000 profile_fibonacci_memoized.py:19(__call__)
        1    0.004    0.004    0.008    0.008 profile:0(print 3, fib_seq(20))
        1    0.004    0.004    0.008    0.008 profile:0(print 1, fib_seq(20))
       21    0.004    0.000    0.004    0.000 profile_fibonacci_memoized.py:26(fib)
        1    0.000    0.000    0.004    0.004 profile:0(print 4, fib_seq(20))
      105    0.000    0.000    0.000    0.000 :0(append)
        5    0.000    0.000    0.000    0.000 :0(setprofile)
      100    0.000    0.000    0.000    0.000 :0(extend)
        1    0.000    0.000    0.000    0.000 profile:0(print 2, fib_seq(20))
        0    0.000             0.000          profile:0(profiler)

Limitando el contenido del reporte:

Ya que estamos estudiando el rendimiento de fib() y fib_seq(), podemos también restringir la salida del reporte para incluir estas funciones usando una expresión regular que coincida con los valores de archivo:línea(función) que queremos.

import profile
import pstats
from profile_fibonacci_memoized import fib, fib_seq

# Lee los 5 archivos con estadísticas en un solo objeto
stats = pstats.Stats('profile_stats_0.stats')
for i in range(1, 5):
    stats.add('profile_stats_%d.stats' % i)
stats.strip_dirs()
stats.sort_stats('cumulative')

# Limita el resulato a líneas con "(fib"
stats.print_stats('\(fib')

La expresión regular incluye un paréntesis izquierdo literal (() para coincidir con la parte del nombre de la función del valor location.

$ python profile_stats_restricted.py
Wed Sep  3 21:22:27 2008    profile_stats_0.stats
Wed Sep  3 21:22:27 2008    profile_stats_1.stats
Wed Sep  3 21:22:27 2008    profile_stats_2.stats
Wed Sep  3 21:22:27 2008    profile_stats_3.stats
Wed Sep  3 21:22:27 2008    profile_stats_4.stats

         489 function calls (351 primitive calls) in 0.032 CPU seconds

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

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
    105/5    0.012    0.000    0.020    0.004 profile_fibonacci_memoized.py:36(fib_seq)
       21    0.004    0.000    0.004    0.000 profile_fibonacci_memoized.py:26(fib)

Grafos de funciones invocadas e invocadoras (caller/callee):

Stats también incluye métodos para mostrar las funciones invocadas y quienes las invocan.

import profile
import pstats
from profile_fibonacci_memoized import fib, fib_seq

# Lee los 5 archivos con estadísticas en un solo objeto
stats = pstats.Stats('profile_stats_0.stats')
for i in range(1, 5):
    stats.add('profile_stats_%d.stats' % i)
stats.strip_dirs()
stats.sort_stats('cumulative')

print 'INVOCACIONES ENTRANTES:'
stats.print_callers('\(fib')

print 'INVOCACIONES SALIENTES:'
stats.print_callees('\(fib')

Los argumentos de print_callers() y print_callees() funciona de la misma manera que los argumentos de restricción de print_stats(). El resultado muestra en invocador (caller), el invocado (callee) y el tiempo acumulado.

$ python profile_stats_callers.py
INVOCACIONES ENTRANTES:
   Ordered by: cumulative time
   List reduced from 13 to 2 due to restriction <'\\(fib'>

Function                                   was called by...
profile_fibonacci_memoized.py:36(fib_seq)  <- <string>:1(<module>)(5)    0.024
                                              profile_fibonacci_memoized.py:36(fib_seq)(100)    0.020
profile_fibonacci_memoized.py:26(fib)      <- profile_fibonacci_memoized.py:19(__call__)(21)    0.008


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

Function                                   called...
profile_fibonacci_memoized.py:36(fib_seq)  -> :0(append)(105)    0.000
                                              :0(extend)(100)    0.000
                                              profile_fibonacci_memoized.py:19(__call__)(105)    0.008
                                              profile_fibonacci_memoized.py:36(fib_seq)(100)    0.020
profile_fibonacci_memoized.py:26(fib)      -> profile_fibonacci_memoized.py:19(__call__)(38)    0.008

Referencias:

Python Module of the Week Home
Descarga el código
profile y cProfile
pstats

La implementación del ejemplo Fibonacci es de Fibonacci numbers (Python) - LiteratePrograms).

El decorador memoize es de Python Decorators: Syntactic Sugar | avinash.vora.

Copyright 2008 Doug Hellmann.


blog comments powered by Disqus

Categorías