Python – rendimiento con variables globales vs local

Todavía soy nuevo en Python, y he estado tratando de mejorar el rendimiento de mi script de Python, por lo que lo probé con y sin variables globales. Lo cronometré y, para mi sorpresa, corrió más rápido con las variables globales declaradas en lugar de pasar las variables locales a las funciones. ¿Que esta pasando? ¿Pensé que la velocidad de ejecución era más rápida con las variables locales? (Sé que los globales no son seguros, sigo siendo curioso).

Los locales deberían ser más rápidos

Según esta página en locales y globales :

Cuando una línea de código solicita el valor de una variable x, Python buscará esa variable en todos los espacios de nombres disponibles, en orden:

  • espacio de nombres local – específico para la función actual o el método de clase. Si la función define una variable local x, o tiene un argumento x, Python usará esto y dejará de buscar.
  • espacio de nombres global – específico para el módulo actual. Si el módulo ha definido una variable, función o clase llamada x, Python lo usará y dejará de buscar.
  • espacio de nombres incorporado – global a todos los módulos. Como último recurso, Python asumirá que x es el nombre de la función o variable incorporada.

En base a eso, asumo que las variables locales son generalmente más rápidas. Mi conjetura es que lo que estás viendo es algo particular acerca de tu guión.

Los locales son más rápidos

Aquí hay un ejemplo trivial que usa una variable local, que toma alrededor de 0.5 segundos en mi máquina (0.3 en Python 3):

def func(): for i in range(10000000): x = 5 func() 

Y la versión global, que toma alrededor de 0.7 (0.5 en Python 3):

 def func(): global x for i in range(1000000): x = 5 func() 

global hace algo extraño a las variables que ya son globales.

Curiosamente, esta versión se ejecuta en 0,8 segundos:

 global x x = 5 for i in range(10000000): x = 5 

Mientras esto se ejecuta en 0.9:

 x = 5 for i in range(10000000): x = 5 

Notará que en ambos casos, x es una variable global (ya que no hay funciones), y ambas son más lentas que el uso de locales. No tengo ni idea de por qué la statement de global x ayudó en este caso.

Esta rareza no ocurre en Python 3 (ambas versiones tardan unos 0.6 segundos).

Mejores métodos de optimización.

Si desea optimizar su progtwig, lo mejor que puede hacer es perfilarlo . Esto le dirá qué es lo que lleva más tiempo, para que pueda concentrarse en eso. Su proceso debe ser algo como:

  1. Ejecute su progtwig con perfiles en.
  2. Mire el perfil en KCacheGrind o un progtwig similar para determinar qué funciones están tardando más tiempo.
  3. En esas funciones:
    • Busque lugares donde pueda almacenar en caché los resultados de las funciones (para que no tenga que hacer tanto trabajo).
    • Busque mejoras algorítmicas como reemplazar funciones recursivas con funciones de forma cerrada, o reemplazar búsquedas de lista con diccionarios.
    • Re-perfil para asegurarse de que la función sigue siendo un problema.
    • Considere el uso de multiprocesamiento .

El tiempo que no está incluyendo es el tiempo del progtwigdor dedicado a rastrear los errores creados cuando el uso de un global tiene un efecto secundario en otra parte de su progtwig. Ese tiempo es muchas veces mayor que el tiempo dedicado a crear y liberar variables locales,

Respuesta simple:

Debido a la naturaleza dinámica de Python, cuando el intérprete se encuentra con una expresión como abc, busca (intentando primero el espacio de nombres local, luego el espacio de nombres global y finalmente el espacio de nombres integrado), luego busca el espacio de nombres de ese objeto para resolver el nombre b, y finalmente busca en el espacio de nombres de ese objeto para resolver el nombre c. Estas búsquedas son razonablemente rápidas; Para las variables locales, las búsquedas son extremadamente rápidas, ya que el intérprete sabe qué variables son locales y puede asignarles una posición conocida en la memoria.

El intérprete sabe qué nombres dentro de sus funciones son locales y les asigna ubicaciones específicas (conocidas) dentro de la memoria de la llamada de función. Esto hace que las referencias a los locales sean mucho más rápidas que a las globales y (más especialmente) a las integradas.

Código de ejemplo para explicar lo mismo:

 >>> glen = len # provides a global reference to a built-in >>> >>> def flocal(): ... name = len ... for i in range(25): ... x = name ... >>> def fglobal(): ... for i in range(25): ... x = glen ... >>> def fbuiltin(): ... for i in range(25): ... x = len ... >>> timeit("flocal()", "from __main__ import flocal") 1.743438959121704 >>> timeit("fglobal()", "from __main__ import fglobal") 2.192162036895752 >>> timeit("fbuiltin()", "from __main__ import fbuiltin") 2.259413003921509 >>> 

Cuando Python comstack una función, la función sabe antes de que se llame si las variables en ella son locales, cierres o globales.

Tenemos varias formas de referenciar variables en funciones:

  • globales
  • cierres
  • locales

Así que vamos a crear este tipo de variables en unas pocas funciones diferentes para que podamos ver por nosotros mismos:

 global_foo = 'foo' def globalfoo(): return global_foo def makeclosurefoo(): boundfoo = 'foo' def innerfoo(): return boundfoo return innerfoo closurefoo = makeclosurefoo() def defaultfoo(foo='foo'): return foo def localfoo(): foo = 'foo' return foo 

Desmontado

Podemos ver que cada función sabe dónde buscar la variable, no es necesario hacerlo en el tiempo de ejecución:

 >>> import dis >>> dis.dis(globalfoo) 2 0 LOAD_GLOBAL 0 (global_foo) 2 RETURN_VALUE >>> dis.dis(closurefoo) 4 0 LOAD_DEREF 0 (boundfoo) 2 RETURN_VALUE >>> dis.dis(defaultfoo) 2 0 LOAD_FAST 0 (foo) 2 RETURN_VALUE >>> dis.dis(localfoo) 2 0 LOAD_CONST 1 ('foo') 2 STORE_FAST 0 (foo) 3 4 LOAD_FAST 0 (foo) 6 RETURN_VALUE 

Podemos ver que actualmente el byte-code para un global es LOAD_GLOBAL , una variable de cierre es LOAD_DEREF , y un local es LOAD_FAST . Estos son detalles de implementación de CPython, y pueden cambiar de una versión a otra, pero es útil poder ver que Python trata cada búsqueda de variable de manera diferente.

Pegar en un intérprete y ver por ti mismo:

 import dis dis.dis(globalfoo) dis.dis(closurefoo) dis.dis(defaultfoo) dis.dis(localfoo) 

Código de prueba

Código de prueba (no dude en probar en su sistema):

 import sys sys.version import timeit min(timeit.repeat(globalfoo)) min(timeit.repeat(closurefoo)) min(timeit.repeat(defaultfoo)) min(timeit.repeat(localfoo)) 

Salida

En Windows, al menos en esta comstackción, parece que los cierres reciben un poco de penalización, y usar un local que es el predeterminado es el más rápido, porque no tiene que asignar el local cada vez:

 >>> import sys >>> sys.version '3.6.1 |Anaconda 4.4.0 (64-bit)| (default, May 11 2017, 13:25:24) [MSC v.1900 64 bit (AMD64)]' >>> import timeit >>> min(timeit.repeat(globalfoo)) 0.0728403456180331 >>> min(timeit.repeat(closurefoo)) 0.07465484920749077 >>> min(timeit.repeat(defaultfoo)) 0.06542038103088998 >>> min(timeit.repeat(localfoo)) 0.06801849537714588 

En Linux:

 >>> import sys >>> sys.version '3.6.4 |Anaconda custom (64-bit)| (default, Mar 13 2018, 01:15:57) \n[GCC 7.2.0]' >>> import timeit >>> min(timeit.repeat(globalfoo)) 0.08560040907468647 >>> min(timeit.repeat(closurefoo)) 0.08592104795388877 >>> min(timeit.repeat(defaultfoo)) 0.06587386003229767 >>> min(timeit.repeat(localfoo)) 0.06887826602905989 

Agregaré otros sistemas cuando tenga la oportunidad de probarlos.