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

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

ce morceau de code en Python s'exécute en (Note: le timing est fait avec la fonction time en 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

puis il court pour un temps beaucoup plus long:

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

pourquoi?

745
demandé sur thedoctar 2012-06-28 13:18:34
la source

3 ответов

vous pourriez vous demander pourquoi il est plus rapide de stocker des variables locales que des variables globales. C'est un Disponible détail d'implémentation.

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 ( et non a dict ) et les noms des variables sont assignés aux index. C'est possible parce que vous ne pouvez pas ajouter dynamiquement des variables locales fonction. Puis extraire une variable locale est littéralement un pointeur de recherche dans la liste et une augmentation de refcount sur le PyObject qui est trivial.

comparez ceci à une recherche globale ( LOAD_GLOBAL ), qui est une vraie recherche dict impliquant un hachage et ainsi de suite. Soit dit en passant, c'est pourquoi vous devez spécifier global i si vous voulez qu'il soit global: si vous assignez jamais à une variable à l'intérieur d'un scope, le compilateur émettra STORE_FAST pour son accès à moins que vous dire de ne pas.

soit dit en passant, les recherches globales sont encore assez optimisées. Attribut lookups foo.bar sont les vraiment lents!

Voici le petit l'illustration à la variable locale de l'efficacité.

454
répondu Katriel 2017-06-16 17:00:19
la source

dans une fonction, le code 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 code 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 niveau supérieur, il est mondial.

pour examiner le bytecode, utilisez le dis module . J'ai été en mesure pour démonter la fonction directement, mais pour démonter le code de niveau supérieur j'ai dû utiliser le compile builtin .

634
répondu ecatmur 2012-06-28 13:29:12
la source

mis à part les variables locales/globales de temps de stockage, opcode prédiction rend la fonction plus rapide.

comme l'expliquent les autres réponses, la fonction utilise l'opcode STORE_FAST dans la boucle. Voici le code 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 quand un programme est lancé, Python exécute chaque opcode l'un après l'autre, en gardant la trace de la pile et en préformant les autres vérifications sur le cadre de la pile après chaque opcode est exécutée. La prédiction Opcode signifie que dans certains cas Python est capable de sauter directement vers le prochain opcode, évitant ainsi une partie de ce overhead.

dans ce cas, chaque fois que Python voit FOR_ITER (le haut de la boucle), Il" prédit "que STORE_FAST est le prochain opcode 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 comprimer les deux opcodes en un seul opcode.

par contre, le STORE_NAME opcode est utilisé dans la boucle au niveau global. Python fait * Non* faire des prédictions similaires quand il voit ce opcode. Au lieu de cela, il doit revenir au sommet de la boucle d'évaluation, ce qui a des implications évidentes pour la vitesse à laquelle la boucle est exécutée.

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

certains opcodes ont tendance à venir en paires, ce qui permet de prédisez le second code quand le premier est lancé. Exemple, GET_ITER est souvent suivi de FOR_ITER . Et FOR_ITER est souvent suivi de STORE_FAST ou UNPACK_SEQUENCE .

vérifier les coûts prévisionnels un seul essai à grande vitesse d'un enregistreur variable par rapport à une constante. Si le couplage a été bonne, l' la prédication interne de la branche du processeur a une forte probabilité de le succès, résultant en une transition presque zéro-frais généraux à la à côté de l'opcode. Une prédiction réussie sauve un voyage à travers la boucle d'éval y compris ses deux branches imprévisibles, le test HAS_ARG et le cas de commutateur. Combiné avec la prédiction interne de la branche du processeur, un le succès PREDICT a pour effet de faire fonctionner les deux codes op comme si c'était un nouveau opcode avec les corps combinés.

nous pouvons voir dans le code source pour le FOR_ITER opcode exactement où la prédiction pour STORE_FAST est faite:

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                                     

la fonction PREDICT s'étend à if (*next_instr == op) goto PRED_##op c'est-à-dire que nous sautons juste au début du code op prévu. Dans ce cas, nous

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 le prochain opcode est prêt à être exécuté. Python continue à travers l'itérable jusqu'à ce qu'il atteigne la fin, ce qui rend la prédiction réussie à chaque fois.

la page wiki de Python contient plus d'informations sur le fonctionnement de la machine virtuelle de CPython.

29
répondu Alex Riley 2018-03-28 12:16:31
la source