¡El sol dice: identifica la función cuello de botella con Python cProfile!
¡Nosotros!
¡Ugh!
¡Acérquense!
¡Miau!
Hola, soy Incompetente.
Últimamente, me divierto mucho escuchando el canal de Masahiro Sakurai, ex-miembro de HAL Laboratory, como si fuera la radio mientras trasteo con el PC.
De hecho, los recuerdos de cuando jugaba con mi 3DS reviven, y los Nintendo Direct del presidente Iwata que veía en aquel entonces siempre eran divertidos. Al volver a verlos, y hablando de otros ejemplos, como Toshio Ikeda de Fujitsu, se ve una gran pasión en los documentales, y es increíble pensar en un mundo donde se viajaba a Estados Unidos tantas veces en aquella época. En aquel entonces también estaba la Guerra de Vietnam, y las sombras de la posguerra de la Segunda Guerra Mundial debían estar presentes, así que, ¿de dónde surgió toda esa energía?
Introducción
No solía prestar mucha atención a la velocidad de ejecución de los lenguajes de script, pero al ejecutar cuckooget, que estoy desarrollando, me empezó a picar la curiosidad.
Parte del código está escrito en Rust y se puede ejecutar dentro del intérprete de Python usando PyO3/maturin.
En términos más sencillos, las funciones escritas en Rust se convierten en una librería de Python y se ejecutan a través de un archivo binario compilado a partir del código Rust. La razón es simple: para procesos que implican cálculos de CPU, parece que ejecutarlos con este archivo binario sería más rápido. Aunque resultó ser más rápido, hay un gran error en esto.
Primero, no realicé pruebas de rendimiento con código puramente Python desde el principio.
Incluso en las "Notas sobre programación en C" escritas por Rob Pike, a quien se le conoce como el padre de Go, se dice:
Regla 1: No se puede saber dónde consumirá tiempo un programa. Los cuellos de botella ocurren en lugares sorprendentes. Por lo tanto, no hagas suposiciones ni optimizaciones de velocidad hasta que hayas identificado claramente dónde está el cuello de botella.
Siendo así, se deben realizar mediciones. Si este es un principio en el lenguaje C de bajo nivel, es impensable que no se aplique a las capas superiores.
A veces olvido estas cosas, así que siento profundamente la necesidad de algo como la filosofía UNIX, que me obliga a revisar periódicamente para disciplinarme.
Dicho esto, como soy humano, aunque lo entiendo, tiendo a obtener valores atípicos...
Medición con cProfile
Al principio, usaba un método primitivo con import time, start_time = time.time() y después de la ejecución de la función end_time = time.time(), print(f"exec time {start_time - end_time}"). Pero pensé, '¡Qué ineficiente es esto, usando una computadora para algo tan básico!', así que busqué algo que mostrara el tiempo de ejecución para cada función.
Parece que hay algo llamado cProfile como parte de la biblioteca estándar.
El siguiente sitio web es fácil de entender:
Analizar el rendimiento del código con Python cProfile
En mi caso, lo verifiqué de la siguiente manera:
python3 -m cProfile -o profile.pstats ./main.py https://soulminingrig.com/ site
De esta manera, el tiempo de ejecución de las funciones ejecutadas por ./main.py se guarda en un archivo binario llamado profile.pstats.
Verificar los resultados de la ejecución de la función
En el sitio web anterior, se usaba un archivo ejecutable de Python, pero es más fácil entrar en el modo de línea de comandos de pstats para verificarlo.
python3 -m pstats profile.pstats
Esto entra en el modo de línea de comandos de pstats, y los resultados de la salida se verifican de la siguiente manera:
profile.pstats% stats 5
Thu Nov 7 20:24:10 2024 profile.pstats
48488696 function calls (47625490 primitive calls) in 51.151 seconds
Random listing order was used
List reduced from 3017 to 5 due to restriction <5>
ncalls tottime percall cumtime percall filename:lineno(function)
1586 0.001 0.000 0.001 0.000 /home/haturatu/.local/lib/python3.12/site-packages/aiohttp/client_reqrep.py:909(__del__)
80 0.000 0.000 0.001 0.000 /usr/lib/python3.12/copy.py:247(_reconstruct)
4 0.000 0.000 0.005 0.001 /usr/lib/python3.12/encodings/__init__.py:71(search_function)
3 0.000 0.000 98.745 32.915 /usr/lib/python3.12/asyncio/base_events.py:651(run_until_complete)
1103 0.001 0.000 0.003 0.000 /usr/lib/python3.12/re/_parser.py:312(_class_escape)
Puedes verificar otros comandos con help.
profile.pstats% help
Documented commands (type help <topic>):
========================================
EOF add callees callers help quit read reverse sort stats strip
En mi caso, generé la salida con stats 100, la abrí con vim en otra terminal, la copié y pegué, y luego salí con quit.
Ahora, examinaremos este archivo de texto guardado después de pegarlo en vim usando awk y sort.
En el caso de tottime
$ awk '{ print $2","$6 }' bench | sort | column -t -s "," | tail
0.009 /usr/lib/python3.12/re/_compiler.py:243(_optimize_charset)
0.010 /usr/lib/python3.12/concurrent/futures/_base.py:428(result)
0.012 /usr/lib/python3.12/asyncio/base_events.py:627(run_forever)
0.013 /usr/lib/python3.12/re/__init__.py:280(_compile)
0.018 /usr/lib/python3.12/re/_compiler.py:37(_compile)
0.022 /usr/lib/python3.12/concurrent/futures/_base.py:497(set_running_or_notify_cancel)
0.030 /home/haturatu/git/devgit/async_web_mirror.py:186(download_and_save_image)
0.031 /usr/lib/python3.12/re/_parser.py:512(_parse)
0.061 /usr/lib/python3.12/concurrent/futures/_base.py:537(set_result)
tottime filename:lineno(function)
De alguna manera, parece que la información de tottime por sí sola es suficiente para una evaluación de rendimiento por función.
¿Es el cuello de botella de Beautiful Soup 4 solo una impresión?
Lo que resultó lento en otros entornos fue search_tag en element.py, llamado desde bs4, que parecía estar ralentizando las cosas.
Lo que me llamó la atención fue esto:
if ((not self.name)
or call_function_with_tag_data
or (markup and self._matches(markup, self.name))
or (not markup and self._matches(markup_name, self.name))):
Es difícil creer que la operación or sea el cuello de botella...
Sin embargo, solo hay 7 lugares donde se evalúa or en element.py, y 3 de ellos se usan aquí.
Algo huele mal aquí.
Y así, como el tema se ha desviado hacia bs4, lo dejaré aquí por ahora.
Hasta la próxima.