Question Pourquoi le code Python s'exécute-t-il plus vite dans une fonction?


def main():
    for i in xrange(10**8):
        pass
main()

Ce morceau de code dans Python s'exécute (Note: Le timing est fait avec la fonction time dans BASH sous Linux.)

real    0m1.841s
user    0m1.828s
sys     0m0.012s

Cependant, si la boucle for n'est pas placée dans une fonction,

for i in xrange(10**8):
    pass

alors ça dure beaucoup plus longtemps:

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

Pourquoi est-ce?


728
2018-06-28 09:18


origine


Réponses:


Vous pourriez demander Pourquoi il est plus rapide de stocker des variables locales que globales. Ceci est un détail d'implémentation de CPython.

Rappelez-vous que CPython est compilé en bytecode, que l'interpréteur exécute. Lorsqu'une fonction est compilée, les variables locales sont stockées dans un tableau de taille fixe (ne pas une dict) et les noms de variables sont affectés aux index. Cela est possible car vous ne pouvez pas ajouter dynamiquement des variables locales à une fonction. Récupérer ensuite une variable locale est littéralement une recherche de pointeur dans la liste et une augmentation de refcount sur le PyObject ce qui est trivial.

Comparez cela à une recherche globale (LOAD_GLOBAL), ce qui est vrai dict recherche impliquant un hachage et ainsi de suite. Incidemment, c'est pourquoi vous devez spécifier global i si vous voulez que ce soit global: si vous affectez une variable à l'intérieur d'une portée, le compilateur émettra STORE_FASTs pour son accès sauf si vous lui dites de ne pas le faire.

En passant, les recherches globales sont encore assez optimisées. Recherches d'attributs foo.bar sont les vraiment ceux qui sont lents!

Voici petit illustration sur l'efficacité variable locale.


441
2018-06-28 10:15



Dans une fonction, le bytecode est

  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        

Au niveau supérieur, le bytecode est

  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 différence est que STORE_FAST est plus rapide (!) que STORE_NAME. C'est parce que dans une fonction, i est un local mais au premier plan c'est un global.

Pour examiner le bytecode, utilisez le dis module. J'ai été capable de démonter la fonction directement, mais pour démonter le code toplevel je devais utiliser le compile intégré.


629
2018-06-28 09:29



En dehors des heures de stockage variables locales / globales, prédiction d'opcode rend la fonction plus rapide.

Comme l'expliquent les autres réponses, la fonction utilise le STORE_FAST opcode dans la boucle. Voici le bytecode pour la boucle de la fonction:

    >>   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

Normalement, lorsqu'un programme est exécuté, Python exécute chaque code d'opération l'un après l'autre, en gardant une trace de la pile et en effectuant d'autres contrôles sur la trame de pile après l'exécution de chaque opcode. La prédiction Opcode signifie que dans certains cas Python est capable de passer directement à l'opcode suivant, évitant ainsi une partie de cette surcharge.

Dans ce cas, chaque fois que Python voit FOR_ITER (le haut de la boucle), il va "prédire" que STORE_FAST est l'opcode suivant qu'il doit exécuter. Python jette alors un coup d'oeil à l'opcode suivant et, si la prédiction était correcte, il saute directement à STORE_FAST. Cela a pour effet de serrer les deux opcodes dans un seul opcode.

D'un autre côté, le STORE_NAME opcode est utilisé dans la boucle au niveau global. Python fait *ne pas* faire des prédictions similaires quand il voit cet opcode. Au lieu de cela, il doit retourner en haut de la boucle d'évaluation, ce qui a des implications évidentes sur la vitesse à laquelle la boucle est exécutée.

Pour donner plus de détails techniques sur cette optimisation, voici une citation du ceval.c fichier (le "moteur" de la machine virtuelle de Python):

Certains opcodes ont tendance à venir en paires, ce qui permet de    prédire le deuxième code lorsque le premier est exécuté. Par exemple,     GET_ITER est souvent suivi par FOR_ITER. Et FOR_ITER est souvent    suivi par STORE_FAST ou UNPACK_SEQUENCE.

La vérification de la prédiction coûte un seul test haute vitesse d'un registre       variable par rapport à une constante. Si l'appariement était bon, alors le       propre prédication de branche interne du processeur a une forte probabilité de       succès, ce qui se traduit par une transition quasi nulle vers le       prochain opcode. Une prédiction réussie sauve un voyage à travers la boucle d'évaluation       y compris ses deux branches imprévisibles, le HAS_ARG test et le       interrupteur-cas. Combiné avec la prédiction de branche interne du processeur,       un succès PREDICT a pour effet de faire fonctionner les deux opcodes comme si       ils étaient un seul nouvel opcode avec les corps combinés.

Nous pouvons voir dans le code source de la FOR_ITER opcode exactement où la prédiction pour STORE_FAST est fait:

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                                     

le PREDICT fonction se développe à if (*next_instr == op) goto PRED_##op c'est-à-dire que nous passons juste au début de l'opcode prédit. Dans ce cas, nous sautons ici:

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 locale est maintenant définie et l'opcode suivant est en cours d'exécution. Python continue à travers l'itérable jusqu'à ce qu'il atteigne la fin, rendant la prédiction réussie à chaque fois.

le Page wiki Python a plus d'informations sur le fonctionnement de la machine virtuelle de CPython.


29
2018-04-05 18:45