Código de perfiles y tiempos
En el proceso de desarrollo de código y creación de canalizaciones de procesamiento de datos, a menudo hay compensaciones que puede hacer entre varias implementaciones. Al principio del desarrollo de su algoritmo, puede ser contraproducente preocuparse por esas cosas. Como dijo Donald Knuth en broma, "Deberíamos olvidarnos de las pequeñas eficiencias, digamos aproximadamente el 97% del tiempo: la optimización prematura es la raíz de todos los males".
Pero una vez que tenga su código funcionando, puede ser útil profundizar un poco en su eficiencia. A veces es útil verificar el tiempo de ejecución de un comando o conjunto de comandos dado; otras veces es útil profundizar en un proceso de varias líneas y determinar dónde se encuentra el cuello de botella en algunas series complicadas de operaciones. IPython proporciona acceso a una amplia gama de funciones para este tipo de sincronización y creación de perfiles de código. Aquí discutiremos los siguientes comandos mágicos de IPython:
%time
: Programa la ejecución de una sola declaración%timeit
: Tiempo de ejecución repetida de una sola declaración para mayor precisión%prun
: Ejecutar código con el perfilador%lprun
: Ejecutar código con el generador de perfiles línea por línea%memit
: Mide el uso de memoria de una sola declaración%mprun
: Ejecutar código con el perfilador de memoria línea por línea
Los últimos cuatro comandos no están incluidos con IPython; necesitará obtener las extensiones line_profiler
y memory_profiler
, que discutiremos en las siguientes secciones.
Fragmentos de código de tiempo: %timeit
y %time
¶
Vimos %timeit
line-magic y %%timeit
cell-magic en la introducción a las funciones mágicas en Comandos mágicos de IPython; se puede usar para cronometrar la ejecución repetida de fragmentos de código:
%timeit sum(range(100))
Tenga en cuenta que debido a que esta operación es tan rápida, %timeit
realiza automáticamente una gran cantidad de repeticiones. Para comandos más lentos, %timeit
se ajustará automáticamente y realizará menos repeticiones:
%%timeit
total = 0
for i in range(1000):
for j in range(1000):
total += i * (-1) ** j
A veces, repetir una operación no es la mejor opción. Por ejemplo, si tenemos una lista que nos gustaría ordenar, podríamos ser engañados por una operación repetida. Ordenar una lista preordenada es mucho más rápido que ordenar una lista sin ordenar, por lo que la repetición sesgará el resultado:
import random
L = [random.random() for i in range(100000)]
%timeit L.sort()
Para esto, la función mágica %time
puede ser una mejor opción. También es una buena opción para comandos de ejecución más prolongada, cuando es poco probable que los retrasos breves relacionados con el sistema afecten el resultado. Midamos la clasificación de una lista sin clasificar y una lista previamente clasificada:
import random
L = [random.random() for i in range(100000)]
print("sorting an unsorted list:")
%time L.sort()
print("sorting an already sorted list:")
%time L.sort()
Observe cuánto más rápido se ordena la lista preordenada, pero observe también cuánto más tarda el tiempo con %time
versus %timeit
, ¡incluso para la lista preclasificada! Esto es el resultado del hecho de que %timeit
hace algunas cosas inteligentes bajo el capó para evitar que las llamadas al sistema interfieran con la sincronización. Por ejemplo, evita la limpieza de objetos de Python no utilizados (conocido como recolección de basura) que de otra manera podrían afectar el tiempo. Por esta razón, los resultados de %timeit
suelen ser notablemente más rápidos que los resultados de %time
.
Para %time
como con %timeit
, el uso de la sintaxis mágica de celda de doble signo de porcentaje permite la sincronización de scripts de varias líneas:
%%time
total = 0
for i in range(1000):
for j in range(1000):
total += i * (-1) ** j
Para obtener más información sobre %time
y %timeit
, así como sus opciones disponibles, utilice la función de ayuda de IPython (es decir, escriba %time?
en el indicador de IPython).
Creación de perfiles de secuencias
de comandos completas: %prun
¶
Un programa se compone de muchas declaraciones individuales y, a veces, cronometrar estas declaraciones en contexto es más importante que cronometrarlas por sí mismas. Python contiene un generador de perfiles de código incorporado (sobre el cual puede leer en la documentación de Python), pero IPython ofrece una forma mucho más conveniente de usar este generador de perfiles, en la forma de la función mágica %prun
.
A modo de ejemplo, definiremos una función simple que hace algunos cálculos:
def sum_of_lists(N):
total = 0
for i in range(5):
L = [j ^ (j >> i) for j in range(N)]
total += sum(L)
return total
Ahora podemos llamar a %prun
con una llamada de función para ver los resultados perfilados:
%prun sum_of_lists(1000000)
En el cuaderno, la salida se imprime en el buscapersonas y se parece a esto:
14 function calls in 0.714 seconds
Ordered by: internal time
ncalls tottime percall cumtime percall filename:lineno(function)
5 0.599 0.120 0.599 0.120 <ipython-input-19>:4(<listcomp>)
5 0.064 0.013 0.064 0.013 {built-in method sum}
1 0.036 0.036 0.699 0.699 <ipython-input-19>:1(sum_of_lists)
1 0.014 0.014 0.714 0.714 <string>:1(<module>)
1 0.000 0.000 0.714 0.714 {built-in method exec}
El resultado es una tabla que indica, en orden de tiempo total en cada llamada de función, dónde la ejecución está gastando más tiempo. En este caso, la mayor parte del tiempo de ejecución está en la comprensión de la lista dentro de sum_of_lists
. A partir de aquí, podríamos empezar a pensar en los cambios que podríamos hacer para mejorar el rendimiento en el algoritmo.
Para obtener más información sobre %prun
, así como sus opciones disponibles, utilice la función de ayuda de IPython (es decir, escriba %prun?
en el indicador de IPython).
Creación de perfiles línea por línea con %lprun
¶
El perfil función por función de %prun
es útil, pero a veces es más conveniente tener un informe de perfil línea por línea. Esto no está integrado en Python o IPython, pero hay un paquete line_profiler
disponible para la instalación que puede hacer esto. Comience usando la herramienta de empaquetado de Python, pip
, para instalar el paquete line_profiler
:
$ pip install line_profiler
A continuación, puede usar IPython para cargar la extensión line_profiler
IPython, que se ofrece como parte de este paquete:
%load_ext line_profiler
Ahora, el comando %lprun
hará un perfil línea por línea de cualquier función; en este caso, necesitamos decirle explícitamente qué funciones estamos interesados en perfilar:
%lprun -f sum_of_lists sum_of_lists(5000)
Como antes, el cuaderno envía el resultado al buscapersonas, pero se parece a esto:
Timer unit: 1e-06 s
Total time: 0.009382 s
File: <ipython-input-19-fa2be176cc3e>
Function: sum_of_lists at line 1
Line # Hits Time Per Hit % Time Line Contents
==============================================================
1 def sum_of_lists(N):
2 1 2 2.0 0.0 total = 0
3 6 8 1.3 0.1 for i in range(5):
4 5 9001 1800.2 95.9 L = [j ^ (j >> i) for j in range(N)]
5 5 371 74.2 4.0 total += sum(L)
6 1 0 0.0 0.0 return total
La información en la parte superior nos da la clave para leer los resultados: el tiempo se reporta en microsegundos y podemos ver dónde pasa la mayor parte del tiempo el programa. En este punto, es posible que podamos usar esta información para modificar aspectos del script y hacer que funcione mejor para nuestro caso de uso deseado.
Para obtener más información sobre %lprun
, así como sus opciones disponibles, utilice la función de ayuda de IPython (es decir, escriba %lprun?
en el indicador de IPython).
Uso de memoria de creación
de perfiles: %memit
y %mprun
¶
Otro aspecto de la creación de perfiles es la cantidad de memoria que utiliza una operación. Esto se puede evaluar con otra extensión de IPython, el memory_profiler
. Al igual que con line_profiler
, comenzamos con pip
-instalando la extensión:
$ pip install memory_profiler
Entonces podemos usar IPython para cargar la extensión:
%load_ext memory_profiler
La extensión del generador de perfiles de memoria contiene dos funciones mágicas útiles: la magia % memit
(que ofrece un equivalente de medición de memoria de %timeit
) y la función %mprun
(que ofrece un equivalente de medición de memoria de %lprun
). La función %memit
se puede utilizar de forma bastante sencilla:
%memit sum_of_lists(1000000)
Vemos que esta función usa alrededor de 100 MB de memoria.
Para una descripción línea por línea del uso de la memoria, podemos usar la magia %mprun
. Desafortunadamente, esta magia funciona solo para funciones definidas en módulos separados en lugar del cuaderno en sí, por lo que comenzaremos usando la magia %%file
para crear un módulo simple llamado mprun_demo.py
, que contiene nuestra función sum_of_lists
, con una adición que hará que nuestros resultados de perfiles de memoria sean más claros:
%%file mprun_demo.py
def sum_of_lists(N):
total = 0
for i in range(5):
L = [j ^ (j >> i) for j in range(N)]
total += sum(L)
del L # remove reference to L
return total
Ahora podemos importar la nueva versión de esta función y ejecutar el perfilador de línea de memoria:
from mprun_demo import sum_of_lists
%mprun -f sum_of_lists sum_of_lists(1000000)
El resultado, impreso en el localizador, nos da un resumen del uso de memoria de la función y se parece a esto:
Filename: ./mprun_demo.py
Line # Mem usage Increment Line Contents
================================================
4 71.9 MiB 0.0 MiB L = [j ^ (j >> i) for j in range(N)]
Filename: ./mprun_demo.py
Line # Mem usage Increment Line Contents
================================================
1 39.0 MiB 0.0 MiB def sum_of_lists(N):
2 39.0 MiB 0.0 MiB total = 0
3 46.5 MiB 7.5 MiB for i in range(5):
4 71.9 MiB 25.4 MiB L = [j ^ (j >> i) for j in range(N)]
5 71.9 MiB 0.0 MiB total += sum(L)
6 46.5 MiB -25.4 MiB del L # remove reference to L
7 39.1 MiB -7.4 MiB return total
Aquí, la columna Increment
nos dice cuánto afecta cada línea al presupuesto total de memoria: observe que cuando creamos y eliminamos la lista L
, estamos agregando aproximadamente 25 MB de uso de memoria . Esto se suma al uso de la memoria en segundo plano del propio intérprete de Python.
Para obtener más información sobre %memit
y %mprun
, así como sus opciones disponibles, utilice la función de ayuda de IPython (es decir, escriba %memit?
en el indicador de IPython).