def main():
for i in xrange(10**8):
pass
main()
Este fragmento de código en Python se ejecuta en (Nota: el tiempo se realiza con la función de tiempo en BASH en Linux).
real 0m1.841s
user 0m1.828s
sys 0m0.012s
Sin embargo, si el bucle for no se coloca dentro de una función,
for i in xrange(10**8):
pass
entonces se ejecuta por mucho más tiempo:
real 0m4.543s
user 0m4.524s
sys 0m0.012s
¿Por qué es esto?
python
performance
profiling
benchmarking
cpython
el jugo
fuente
fuente
Respuestas:
Puede preguntar por qué es más rápido almacenar variables locales que globales. Este es un detalle de implementación de CPython.
Recuerde que CPython se compila en bytecode, que ejecuta el intérprete. Cuando se compila una función, las variables locales se almacenan en una matriz de tamaño fijo ( no a
dict
) y los nombres de las variables se asignan a los índices. Esto es posible porque no puede agregar dinámicamente variables locales a una función. Luego, recuperar una variable local es literalmente una búsqueda de puntero en la lista y un aumento de recuento en elPyObject
que es trivial.Contraste esto con una búsqueda global (
LOAD_GLOBAL
), que es unadict
búsqueda verdadera que implica un hash, etc. Por cierto, es por eso que debe especificarglobal i
si desea que sea global: si alguna vez asigna a una variable dentro de un ámbito, el compilador emitiráSTORE_FAST
s para su acceso a menos que se lo indique.Por cierto, las búsquedas globales todavía están bastante optimizadas. ¡Las búsquedas de atributos
foo.bar
son realmente lentas!Aquí hay una pequeña ilustración sobre la eficiencia variable local.
fuente
def foo_func: x = 5
,x
es local para una función. El accesox
es local.foo = SomeClass()
,foo.bar
es el acceso al atributo.val = 5
global es global. En cuanto a la velocidad local> global> atributo de acuerdo con lo que he leído aquí. Por lo tanto el accesox
enfoo_func
es el más rápido, seguido porval
, seguido defoo.bar
.foo.attr
no es una búsqueda local porque en el contexto de esta convo, estamos hablando de búsquedas locales como una búsqueda de una variable que pertenece a una función.globals()
función. Si desea más información que esa, es posible que deba comenzar a buscar el código fuente de Python. Y CPython es solo el nombre de la implementación habitual de Python, por lo que probablemente ya lo esté utilizando.Dentro de una función, el código de bytes es:
En el nivel superior, el código de bytes es:
La diferencia es que
STORE_FAST
es más rápido (!) QueSTORE_NAME
. Esto se debe a que en una función,i
es un local, pero a nivel global es un global.Para examinar el código de bytes, use el
dis
módulo . Pude desmontar la función directamente, pero para desmontar el código de nivel superior tuve que usar elcompile
incorporado .fuente
global i
en lamain
función hace que los tiempos de ejecución sean equivalentes.locals()
, vía ,inspect.getframe()
etc.). Buscar un elemento de matriz por un entero constante es mucho más rápido que buscar un dict.Además de los tiempos de almacenamiento de variables locales / globales, la predicción de código de operación hace que la función sea más rápida.
Como explican las otras respuestas, la función usa el
STORE_FAST
código de operación en el bucle. Aquí está el código de bytes para el bucle de la función:Normalmente, cuando se ejecuta un programa, Python ejecuta cada código de operación uno tras otro, haciendo un seguimiento de la pila y realizando otras comprobaciones en el marco de la pila después de ejecutar cada código de operación. La predicción del código de operación significa que, en ciertos casos, Python puede saltar directamente al siguiente código de operación, evitando así parte de esta sobrecarga.
En este caso, cada vez que Python vea
FOR_ITER
(la parte superior del bucle), "predecirá"STORE_FAST
cuál es el próximo código de operación que debe ejecutar. Python luego mira el siguiente código de operación y, si la predicción fue correcta, salta directamente aSTORE_FAST
. Esto tiene el efecto de comprimir los dos códigos de operación en un solo código de operación.Por otro lado, el
STORE_NAME
código de operación se usa en el ciclo a nivel global. Python * no * hace predicciones similares cuando ve este código de operación. En cambio, debe volver a la parte superior del ciclo de evaluación, lo que tiene implicaciones obvias para la velocidad a la que se ejecuta el ciclo.Para dar más detalles técnicos sobre esta optimización, aquí hay una cita del
ceval.c
archivo (el "motor" de la máquina virtual de Python):Podemos ver en el código fuente del código de
FOR_ITER
operación exactamente dónde se realiza la predicciónSTORE_FAST
:La
PREDICT
función se expande a,if (*next_instr == op) goto PRED_##op
es decir, simplemente saltamos al inicio del código de operación predicho. En este caso, saltamos aquí:La variable local ahora está configurada y el siguiente código operativo está en ejecución. Python continúa a través del iterable hasta que llega al final, haciendo la predicción exitosa cada vez.
La página wiki de Python tiene más información sobre cómo funciona la máquina virtual de CPython.
fuente
HAS_ARG
prueba nunca ocurre (excepto cuando el rastreo de bajo nivel está habilitado tanto en la compilación como en el tiempo de ejecución, lo que no ocurre en la construcción normal), dejando solo un salto impredecible.PREDICT
macro está completamente deshabilitada; en su lugar, la mayoría de los casos terminan en unaDISPATCH
rama directa. Pero en las CPU de predicción de bifurcación, el efecto es similar al de laPREDICT
bifurcación (y la predicción) es por código de operación, lo que aumenta las probabilidades de una predicción de bifurcación exitosa.