Código de perfiles y tiempos

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:


In [1]:
%timeit sum(range(100))
100000 loops, best of 3: 1.54 µs per loop


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:


In [2]:
%%timeit
total = 0
for i in range(1000):
    for j in range(1000):
        total += i * (-1) ** j
1 loops, best of 3: 407 ms per loop


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:


In [3]:
import random
L = [random.random() for i in range(100000)]
%timeit L.sort()
100 loops, best of 3: 1.9 ms per loop


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:


In [4]:
import random
L = [random.random() for i in range(100000)]
print("sorting an unsorted list:")
%time L.sort()
sorting an unsorted list:
CPU times: user 40.6 ms, sys: 896 µs, total: 41.5 ms
Wall time: 41.5 ms


In [5]:
print("sorting an already sorted list:")
%time L.sort()
sorting an already sorted list:
CPU times: user 8.18 ms, sys: 10 µs, total: 8.19 ms
Wall time: 8.24 ms


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:


In [6]:
%%time
total = 0
for i in range(1000):
    for j in range(1000):
        total += i * (-1) ** j
CPU times: user 504 ms, sys: 979 µs, total: 505 ms
Wall time: 505 ms


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:


In [7]:
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:


In [8]:
%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:


In [9]:
%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:


In [10]:
%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:


In [12]:
%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:


In [13]:
%memit sum_of_lists(1000000)
peak memory: 100.08 MiB, increment: 61.36 MiB


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:


In [14]:
%%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
Overwriting mprun_demo.py


Ahora podemos importar la nueva versión de esta función y ejecutar el perfilador de línea de memoria:


In [15]:
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).

JeshuaNomics

JeshuaNomics - DataScience es una web de divulgación donde se puede encontrar material formativo en ciencia de datos y programación estadística (R, Python y SQL).

Publicar un comentario (0)
Artículo Anterior Artículo Siguiente