Supongamos que su programa Python es lento y descubre que esto se debe
solo en parte a la falta de recursos del procesador . ¿Cómo averiguo qué partes del código se ven obligadas a esperar algo que no se aplica a la CPU?

Después de leer el material, cuya traducción publicamos hoy, aprenderá a escribir sus propios perfiladores para el código Python. Estamos hablando de herramientas que detectarán lugares en el código que están inactivos mientras esperan la liberación de ciertos recursos. En particular, discutiremos lo siguiente aquí:
- ¿Qué puede esperar el programa?
- Perfilar el uso de recursos que no son recursos de CPU.
- Perfiles de cambios de contexto no intencionales.
¿Qué espera el programa?
En esos momentos cuando el programa no está ocupado con cálculos intensivos utilizando el procesador, parece estar esperando algo. Esto es lo que puede causar la inacción del programa:
- Recursos de red. Esto puede incluir esperar la finalización de las búsquedas de DNS, esperar una respuesta de un recurso de red, esperar que se carguen algunos datos, etc.
- Disco duro La lectura de datos desde el disco duro puede llevar algún tiempo. Lo mismo se puede decir sobre escribir en el disco. A veces, las operaciones de lectura o escritura se realizan solo usando un caché ubicado en la RAM. Con este enfoque, todo sucede bastante rápido. Pero a veces, cuando un programa interactúa directamente con un disco, tales operaciones resultan ser bastante lentas.
- Cerraduras Un programa puede esperar para desbloquear un hilo o proceso.
- Suspensión de trabajo. A veces, un programa puede pausar deliberadamente el trabajo, por ejemplo, pausar entre intentos de realizar alguna acción.
¿Cómo encontrar esos lugares de programas en los que sucede algo que afecta gravemente el rendimiento?
Método número 1: análisis del tiempo durante el cual el programa no usa el procesador
El perfilador incorporado de Python,
cProfile
, puede recopilar datos sobre muchos indicadores diferentes relacionados con el funcionamiento de los programas. Debido a esto, se puede utilizar para crear una herramienta con la que pueda analizar el tiempo durante el cual el programa no utiliza los recursos del procesador.
El sistema operativo puede
decirnos exactamente cuánto tiempo de procesador usó el programa.
Imagine que estamos perfilando un programa de subproceso único. Los programas multiproceso son más difíciles de perfilar, y describir este proceso tampoco es fácil. Si el programa se ejecutó durante 9 segundos y al mismo tiempo utilizó el procesador durante 7,5 segundos, esto significa que pasó 1,5 segundos esperando.
Primero, cree un temporizador que mida el tiempo de espera:
import os def not_cpu_time(): times = os.times() return times.elapsed - (times.system + times.user)
Luego cree un generador de perfiles que analice esta vez:
import cProfile, pstats def profile_not_cpu_time(f, *args, **kwargs): prof = cProfile.Profile(not_cpu_time) prof.runcall(f, *args, **kwargs) result = pstats.Stats(prof) result.sort_stats("time") result.print_stats()
Después de eso, puede perfilar varias funciones:
>>> profile_not_cpu_time( ... lambda: urlopen("https://pythonspeed.com").read()) ncalls tottime percall filename:lineno(function) 3 0.050 0.017 _ssl._SSLSocket.read 1 0.040 0.040 _socket.getaddrinfo 1 0.020 0.020 _socket.socket.connect 1 0.010 0.010 _ssl._SSLSocket.do_handshake 342 0.010 0.000 find.str 192 0.010 0.000 append.list
Los resultados nos permiten concluir que la mayor parte del tiempo se dedicó a leer datos del socket, pero llevó un tiempo realizar búsquedas DNS (
getaddrinfo
), así como realizar protocolos TCP (
connect
) y TLS / SSL.
Como nos aseguramos de investigar aquellos períodos de la operación del programa en los que no utiliza los recursos del procesador, sabemos que todo esto es tiempo de espera puro, es decir, el momento en que el programa no está ocupado con ningún cálculo.
¿Por qué hay tiempo registrado para
str.find
y
list.append
? Al realizar tales operaciones, el programa no tiene nada que esperar, por lo que la explicación parece plausible, según la cual estamos lidiando con una situación en la que no se realizó todo el proceso. Tal vez, esperando la finalización de algún otro proceso, o esperando la finalización de la carga de datos en la memoria desde el archivo de intercambio. Esto indica que se dedicó algún tiempo a realizar estas operaciones, que no es parte del tiempo del procesador.
Además, quiero señalar que he visto informes que contienen pequeños fragmentos negativos de tiempo. Esto implica una cierta discrepancia entre el tiempo transcurrido y el tiempo del procesador, pero no espero que esto tenga un impacto significativo en el análisis de programas más complejos.
Método número 2: análisis del número de cambios de contexto intencionales
El problema con la medición del tiempo que el programa dedica a esperar algo es que, al realizar diferentes sesiones de medición para el mismo programa, puede variar debido a algo que está fuera del alcance del programa. A veces, las consultas DNS pueden ser más lentas de lo habitual. A veces, más lentamente de lo habitual, se pueden cargar algunos datos. Por lo tanto, sería útil utilizar algunos indicadores más predecibles que no están vinculados a la velocidad de lo que rodea el programa.
Una forma de hacerlo es calcular cuántas operaciones que tienen que esperar han completado el proceso. Es decir, estamos hablando de calcular el número de períodos de espera, y no el tiempo dedicado a esperar algo.
Un proceso puede dejar de usar los recursos del procesador por dos razones:
- Cada vez que un proceso realiza una operación que no finaliza instantáneamente, por ejemplo, lee datos de un socket, hace una pausa, etc., esto es equivalente a lo que le dice al sistema operativo: "Despiértame cuando pueda continuar trabajando". Este es el llamado "cambio de contexto deliberado": el procesador puede cambiar a otro proceso hasta que los datos aparezcan en el socket, o hasta que nuestro proceso salga del modo de espera, así como en otros casos similares.
- El “cambio de contexto no intencional” es una situación en la cual el sistema operativo detiene temporalmente un proceso, permitiendo que otro proceso aproveche los recursos del procesador.
Vamos a perfilar los cambios de contexto intencionales.
Escribamos un generador de perfiles que cuente los cambios de contexto intencionales utilizando la biblioteca
psutil
:
import psutil _current_process = psutil.Process() def profile_voluntary_switches(f, *args, **kwargs): prof = cProfile.Profile( lambda: _current_process.num_ctx_switches().voluntary) prof.runcall(f, *args, **kwargs) result = pstats.Stats(prof) result.sort_stats("time") result.print_stats()
Ahora, perfilemos nuevamente el código que funciona con la red:
>>> profile_voluntary_switches( ... lambda: urlopen("https://pythonspeed.com").read()) ncalls tottime percall filename:lineno(function) 3 7.000 2.333 _ssl._SSLSocket.read 1 2.000 2.000 _ssl._SSLSocket.do_handshake 1 2.000 2.000 _socket.getaddrinfo 1 1.000 1.000 _ssl._SSLContext.set_default_verify_path 1 1.000 1.000 _socket.socket.connect
Ahora, en lugar de los datos del tiempo de espera, podemos ver información sobre la cantidad de cambios de contexto intencionales que ocurrieron.
Tenga en cuenta que a veces puede ver cambios de contexto intencionales en lugares inesperados. Creo que esto sucede cuando los datos del archivo de la página se están cargando debido a errores de la página de memoria.
Resumen
El uso de la técnica de creación de perfiles de código descrita aquí crea una cierta carga adicional en el sistema, lo que ralentiza enormemente el programa. Sin embargo, en la mayoría de los casos, esto no debería conducir a una distorsión significativa de los resultados debido al hecho de que no analizamos el uso de los recursos del procesador.
En general, se puede observar que cualquier indicador medible relacionado con el trabajo del programa se presta a la elaboración de perfiles. Por ejemplo, lo siguiente:
- El número de lecturas (
psutil.Process().read_count
) y escrituras ( psutil.Process().write_count
). - En Linux, el número total de bytes leídos y escritos (psutil.
Process().read_chars
). - Indicadores de asignación de memoria (la realización de dicho análisis requerirá cierto esfuerzo; esto se puede hacer usando jemalloc ).
Los detalles sobre los dos primeros elementos de esta lista se pueden encontrar en la documentación de
psutil .
Estimados lectores! ¿Cómo perfilas tus aplicaciones Python?
