diferencia de rendimiento global o local del espacio de nombres

¿Por qué es que la ejecución de un conjunto de comandos en una función:

def main(): [do stuff] return something print(main()) 

tenderá a ejecutarse de 1.5x a 3x veces más rápido en Python que ejecutando comandos en el nivel superior:

 [do stuff] print(something) 

De hecho, la diferencia depende en gran medida de lo que realmente hace “hacer cosas” y principalmente de cuántas veces accede a los nombres que están definidos / usados. Dado que el código es similar, existe una diferencia fundamental entre estos dos casos:

  • En las funciones, el código de bytes para cargar / almacenar nombres se realiza con LOAD_FAST / STORE_FAST .
  • En el ámbito de nivel superior (es decir, módulo), los mismos comandos se ejecutan con LOAD_NAME / STORE_NAME que son más lentos.

Esto se puede ver en los siguientes casos, usaré un bucle for para asegurarme de que las búsquedas de las variables definidas se realicen varias veces .

Función y LOAD_FAST/STORE_FAST :

Definimos una función simple que hace algunas cosas realmente tontas:

 def main(): b = 20 for i in range(1000000): z = 10 * b return z 

Salida generada por dis.dis :

 dis.dis(main) # [/snipped output/] 18 GET_ITER >> 19 FOR_ITER 16 (to 38) 22 STORE_FAST 1 (i) 25 LOAD_CONST 3 (10) 28 LOAD_FAST 0 (b) 31 BINARY_MULTIPLY 32 STORE_FAST 2 (z) 35 JUMP_ABSOLUTE 19 >> 38 POP_BLOCK # [/snipped output/] 

Lo que hay que tener en cuenta aquí son los comandos LOAD_FAST/STORE_FAST en las compensaciones 28 y 32 , que se utilizan para acceder al nombre b utilizado en la operación BINARY_MULTIPLY y almacenar el nombre z , respectivamente. Como su nombre de código de byte lo indica, son la versión rápida de la familia LOAD_*/STORE_* .


Módulos y LOAD_NAME/STORE_NAME :

Ahora, veamos la salida de dis para nuestra versión del módulo de la función anterior:

 # compile the module m = compile(open('main.py', 'r').read(), "main", "exec") dis.dis(m) # [/snipped output/] 18 GET_ITER >> 19 FOR_ITER 16 (to 38) 22 STORE_NAME 2 (i) 25 LOAD_NAME 3 (z) 28 LOAD_NAME 0 (b) 31 BINARY_MULTIPLY 32 STORE_NAME 3 (z) 35 JUMP_ABSOLUTE 19 >> 38 POP_BLOCK # [/snipped output/] 

Por aquí tenemos varias llamadas a LOAD_NAME/STORE_NAME , que , como se mencionó anteriormente, son comandos más lentos de ejecutar .

En este caso, habrá una clara diferencia en el tiempo de ejecución , principalmente porque Python debe evaluar LOAD_NAME/STORE_NAME y LOAD_FAST/STORE_FAST varias veces (debido al bucle for que agregué) y, como resultado, la sobrecarga introducida cada vez El código para cada código de byte que se ejecute se acumulará .

Progtwigción de la ejecución ‘como módulo’:

 start_time = time.time() b = 20 for i in range(1000000): z = 10 *b print(z) print("Time: ", time.time() - start_time) 200 Time: 0.15162253379821777 

Progtwigndo la ejecución como una función:

 start_time = time.time() print(main()) print("Time: ", time.time() - start_time) 200 Time: 0.08665871620178223 

Si realiza ciclos de time en un range menor (por ejemplo, for i in range(1000) ) notará que la versión del ‘módulo’ es más rápida. Esto sucede porque la sobrecarga introducida por la necesidad de llamar a la función main() es mayor que la introducida por las *_FAST vs *_NAME . Así que es en gran medida en relación con la cantidad de trabajo que se realiza.

Entonces, el verdadero culpable aquí, y la razón por la cual esta diferencia es evidente, es el bucle for utilizado. Por lo general, tiene 0 razones para poner un bucle intensivo como ese en el nivel superior de su script. Moverlo en una función y evitar el uso de variables globales , está diseñado para ser más eficiente.


Puede echar un vistazo al código ejecutado para cada uno de los códigos de bytes. Vincularé la fuente de la versión 3.5 de Python aquí, aunque estoy bastante seguro de que 2.7 no difiere mucho. La evaluación de bytecode se realiza en Python/ceval.c específicamente en la función PyEval_EvalFrameEx :

  • LOAD_FAST sourceSTORE_FAST source
  • LOAD_NAME sourceSTORE_NAME source

Como verá, los *_FAST byte *_FAST simplemente obtienen el valor almacenado / cargado utilizando una tabla de símbolos locales de fastlocals se encuentra dentro de los objetos del marco .