Pregunta ¿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 en (Nota: El tiempo se hace 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 funciona durante mucho más tiempo:

real    0m4.543s
user    0m4.524s
sys     0m0.012s

¿Por qué es esto?


728
2018-06-28 09:18


origen


Respuestas:


Usted 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 el intérprete ejecuta. Cuando se compila una función, las variables locales se almacenan en una matriz de tamaño fijo (no un 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 el PyObject que es trivial.

Contraste esto con una búsqueda global (LOAD_GLOBAL), que es un verdadero dict búsqueda que implica un hash y así sucesivamente. Por cierto, esta es la razón por la que necesita especificar global i si quieres que sea global: si alguna vez asignas una variable dentro de un ámbito, el compilador emitirá STORE_FASTs para su acceso a menos que usted le diga que no lo haga.

Por cierto, las búsquedas globales todavía están bastante optimizadas. Búsquedas de atributos foo.bar son el De Verdad ¡lentos!

Aquí hay un pequeño ilustración en la eficiencia variable local.


441
2018-06-28 10:15



Dentro de una función, el bytecode 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 bytecode 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 es porque en una función, i es local, pero a nivel general es global.

Para examinar bytecode, use el dis módulo. Pude desmontar la función directamente, pero para desmontar el código toplevel tuve que usar el compile incorporado.


629
2018-06-28 09:29



Además de los tiempos de almacenamiento de variables locales / globales, predicción de código de operación hace la función más rápida.

Como explican las otras respuestas, la función usa el STORE_FAST opcode en el ciclo Aquí está el bytecode para el bucle de la función:

    >>   13 FOR_ITER                 6 (to 22)   # get next value from iterator
         16 STORE_FAST               0 (x)       # set local variable
         19 JUMP_ABSOLUTE           13           # back to FOR_ITER

Normalmente, cuando se ejecuta un programa, Python ejecuta cada código de operación uno después del 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 ve FOR_ITER (la parte superior del ciclo), "predecirá" que STORE_FAST es el siguiente código de operación que tiene que ejecutar. Python luego echa un vistazo al siguiente código de operación y, si la predicción fue correcta, salta directamente a STORE_FAST. Esto tiene el efecto de exprimir los dos códigos de operación en un único código de operación.

Por otro lado, el STORE_NAME opcode se usa en el ciclo a nivel global. Python lo hace *no* hacer predicciones similares cuando vea este código de operación. En cambio, debe volver al principio del ciclo de evaluación, que tiene implicaciones obvias para la velocidad a la que se ejecuta el ciclo.

Para dar algunos detalles más técnicos sobre esta optimización, aquí hay una cita de la ceval.c archivo (el "motor" de la máquina virtual de Python):

Algunos códigos de operación tienden a aparecer en pares, lo que hace posible    predecir el segundo código cuando se ejecuta el primero. Por ejemplo,     GET_ITER a menudo es seguido por FOR_ITER. Y FOR_ITER es seguido    seguido por STORE_FAST o UNPACK_SEQUENCE.

Verificación de la predicción cuesta una sola prueba de alta velocidad de un registro       variable contra una constante. Si el emparejamiento era bueno, entonces el       la predicción de la rama interna del procesador tiene una alta probabilidad de       éxito, lo que resulta en una transición de casi cero a la       próximo código de operación. Una predicción exitosa salva un viaje a través del ciclo eval       incluyendo sus dos ramas impredecibles, la HAS_ARG prueba y la       Switch-case. Combinado con la predicción de bifurcación interna del procesador,       un éxito PREDICT tiene el efecto de hacer funcionar los dos códigos de operación como si       eran un único código de operación nuevo con los cuerpos combinados.

Podemos ver en el código fuente para el FOR_ITER código de operación exactamente donde la predicción de STORE_FAST está hecho:

case FOR_ITER:                         // the FOR_ITER opcode case
    v = TOP();
    x = (*v->ob_type->tp_iternext)(v); // x is the next value from iterator
    if (x != NULL) {                     
        PUSH(x);                       // put x on top of the stack
        PREDICT(STORE_FAST);           // predict STORE_FAST will follow - success!
        PREDICT(UNPACK_SEQUENCE);      // this and everything below is skipped
        continue;
    }
    // error-checking and more code for when the iterator ends normally                                     

los PREDICT la función se expande a if (*next_instr == op) goto PRED_##op es decir, solo saltamos al comienzo del código de operación predicho. En este caso, saltamos aquí:

PREDICTED_WITH_ARG(STORE_FAST);
case STORE_FAST:
    v = POP();                     // pop x back off the stack
    SETLOCAL(oparg, v);            // set it as the new local variable
    goto fast_next_opcode;

La variable local ahora está configurada y el siguiente código de operación está listo para su ejecución. Python continúa a través de iterable hasta que llega al final, haciendo la predicción exitosa cada vez.

los Página wiki de Python tiene más información sobre cómo funciona la máquina virtual de CPython.


29
2018-04-05 18:45