Módulo dis de Python y convolución de constantes

Hola a todos Hoy queremos compartir otra traducción preparada en anticipación al lanzamiento del curso "Desarrollador web en Python" . Vamos!



Me sorprendió mucho cuando descubrí que


>>> pow(3,89) 

trabaja más lento que


 >>> 3**89 

Traté de encontrar alguna explicación aceptable, pero no pude. Seguí el tiempo de ejecución de estas dos expresiones usando el módulo timeit de Python 3:


 $ python3 -m timeit 'pow(3,89)' 500000 loops, best of 5: 688 nsec per loop $ python3 -m timeit '3**89' 500000 loops, best of 5: 519 nsec per loop 

La diferencia es pequeña. Solo 0.1 μs, pero me persiguió. Si no puedo explicar nada en la programación, empiezo a sufrir de insomnio.


Encontré la respuesta usando el feed Python IRC en Freenode. La razón por la que pow funciona un poco más lento es porque CPython ya tiene un paso adicional para cargar pow desde el espacio de nombres. Mientras que cuando se llama 3 ** 9, dicha carga no es necesaria en principio. También significa que esta diferencia de tiempo permanecerá más o menos constante si aumentan los valores de entrada.


La hipótesis fue confirmada:


 $ python3 -m timeit 'pow(3,9999)' 5000 loops, best of 5: 58.5 usec per loop $ python3 -m timeit '3**9999' 5000 loops, best of 5: 57.3 usec per loop 

En el proceso de encontrar una solución a este problema, también aprendí sobre el módulo dis. Le permite descompilar el código de bytes de Python y aprenderlo. Fue un descubrimiento extremadamente emocionante, ya que recientemente he estado estudiando ingeniería inversa de archivos binarios, y el módulo descubierto fue útil en este asunto.


Descompilé el bytecode de las expresiones anteriores y obtuve lo siguiente:


 >>> import dis >>> dis.dis('pow(3,89)') # 1 0 LOAD_NAME 0 (pow) # 2 LOAD_CONST 0 (3) # 4 LOAD_CONST 1 (89) # 6 CALL_FUNCTION 2 # 8 RETURN_VALUE >>> dis.dis('3**64') # 1 0 LOAD_CONST 0 (3433683820292512484657849089281) # 2 RETURN_VALUE >>> dis.dis('3**65') # 1 0 LOAD_CONST 0 (3) # 2 LOAD_CONST 1 (65) # 4 BINARY_POWER # 6 RETURN_VALUE 

Puede leer acerca de cómo entender correctamente la salida de dis.dis consultando esta respuesta en Stackoverflow.


Ok, volviendo al código. Descompilar pow tiene sentido. El código de bytes carga pow desde el espacio de nombres, carga en los registros 3 y 89, y finalmente llama a la función pow. Pero, ¿por qué son diferentes los resultados de las siguientes dos descompilaciones? ¡Después de todo, todo lo que hemos cambiado es el valor del exponente de 64 a 65!


Esta pregunta me presentó a otro nuevo concepto llamado "convolución de constantes". Significa que cuando tenemos una expresión constante, Python calcula su valor en la etapa de compilación, por lo que cuando ejecuta el programa, no tomará mucho tiempo, porque Python usa el valor ya calculado. Mira esto:


 def one_plue_one(): return 1+1 # --vs-- def one_plue_one(): return 2 

Python compila la primera función en la segunda y la usa cuando ejecuta el código. No está mal, ¿eh?


Entonces, ¿por qué la convolución de constantes funciona para 3 ** 64, pero no para 3 ** 65? Bueno, no lo se. Esto probablemente esté relacionado de alguna manera con la limitación del número de grados previamente calculados por el sistema en la memoria. Podría estar equivocado El siguiente paso que planeo dar es profundizar en el código fuente de Python en mi tiempo libre e intentar entender lo que está sucediendo. Todavía estoy buscando una respuesta a mi pregunta, así que si tienes alguna idea, compártela en los comentarios.


Quiero que se inspire en esta publicación para encontrar una solución a sus problemas usted mismo. Nunca sabes a dónde te llevarán las respuestas. Finalmente, puedes aprender algo completamente nuevo, como me pasó a mí. ¡Espero que la llama de la curiosidad siga ardiendo en ti!


¿Has notado cosas similares? Esperando sus comentarios!

Source: https://habr.com/ru/post/460143/


All Articles