¿Por qué el código Python se ejecuta más rápido en una función?
def main():
for i in xrange(10**8):
pass
main()
Este fragmento de código en Python se ejecuta (Nota: la sincronización 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
luego se ejecuta durante mucho más tiempo:
real 0m4.543s
user 0m4.524s
sys 0m0.012s
¿Por qué es esto?
Dentro de una función, el código de bytes es:
2 0 SETUP_LOOP 20 (to 23)
3 LOAD_GLOBAL 0 (xrange)
6 LOAD_CONST 3 (100000000)
9 CALL_FUNCTION 1
12 GET_ITER
>> 13 FOR_ITER 6 (to 22)
16 STORE_FAST 0 (i)
3 19 JUMP_ABSOLUTE 13
>> 22 POP_BLOCK
>> 23 LOAD_CONST 0 (None)
26 RETURN_VALUE
En el nivel superior, el código de bytes es:
1 0 SETUP_LOOP 20 (to 23)
3 LOAD_NAME 0 (xrange)
6 LOAD_CONST 3 (100000000)
9 CALL_FUNCTION 1
12 GET_ITER
>> 13 FOR_ITER 6 (to 22)
16 STORE_NAME 1 (i)
2 19 JUMP_ABSOLUTE 13
>> 22 POP_BLOCK
>> 23 LOAD_CONST 2 (None)
26 RETURN_VALUE
La diferencia es que STORE_FAST
es más rápido (!) que STORE_NAME
. Esto se debe a que en una función i
es local pero en el nivel superior es global.
Para examinar el código de bytes, utilice el dis
módulo . Pude desmontar la función directamente, pero para desmontar el código de nivel superior tuve que usar el compile
archivo incorporado .
Quizás se pregunte 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 código de bytes, que ejecuta el intérprete. Cuando se compila una función, las variables locales se almacenan en una matriz de tamaño fijo ( no una dict
) y los nombres de las variables se asignan a los índices. Esto es posible porque no se pueden 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, lo PyObject
cual es trivial.
Compare esto con una búsqueda global ( LOAD_GLOBAL
), que es una dict
búsqueda verdadera que involucra un hash, etc. Por cierto, es por eso que necesitas especificar global i
si quieres que sea global: si alguna vez asignas una variable dentro de un alcance, el compilador emitirá STORE_FAST
s para su acceso a menos que le indiques que no lo haga.
Por cierto, las búsquedas globales todavía están bastante optimizadas. ¡ Las búsquedas de atributos foo.bar
son las realmente lentas!
A continuación se muestra una pequeña ilustración sobre la eficiencia de las variables locales.