¡El sol dice: identifica la función cuello de botella con Python cProfile!

9 min

language: ja bn en es hi pt ru zh-cn zh-tw

¡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.

Related Posts