web-dev-qa-db-fra.com

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 dans Python est exécuté (Remarque: le minutage est effectué 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 cela dure beaucoup plus longtemps:

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

Pourquoi est-ce?

796
thedoctar

Vous pourriez vous demander pourquoi il est plus rapide de stocker des variables locales que des globales. Ceci est un détail d'implémentation 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 ( et non a dict) et des noms de variables sont attribués. index. Cela est possible car vous ne pouvez pas ajouter de manière dynamique des variables locales à une fonction. Ensuite, récupérer une variable locale est littéralement une recherche de pointeur dans la liste et une augmentation de refcount sur le PyObject qui est trivial.

Comparez cela à une recherche globale (LOAD_GLOBAL), qui est une véritable recherche dict impliquant un hachage, etc. C'est d'ailleurs pour cette raison que vous devez spécifier global i si vous souhaitez qu'elle soit globale: si vous affectez une variable à l'intérieur d'une portée, le compilateur émettra STORE_FASTs pour son accès, sauf indication contraire de votre part. à.

À propos, les recherches globales sont encore assez optimisées. Les recherches d'attributs foo.bar sont les vraiment lentes!

Voici petit illustration sur l'efficacité variable locale.

501
Katriel

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 plus haut niveau, 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 . En effet, dans une fonction, i est un local mais au niveau supérieur, c'est un global.

Pour examiner le bytecode, utilisez le module dis . J'ai pu désassembler directement la fonction, mais pour désassembler le code de niveau supérieur, je devais utiliser le compile intégré .

651
ecatmur

Outre les temps de stockage des variables locales/globales, la prédiction d'opcode accélère la fonction.

Comme l'expliquent les autres réponses, la fonction utilise l'opcode _STORE_FAST_ 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 opération les uns après les autres, en gardant une trace de la pile et en effectuant d'autres contrôles sur le cadre de la pile après l'exécution de chaque code opération. La prédiction de l'opcode signifie que, dans certains cas, Python peut passer directement à l'opcode suivant, évitant ainsi une partie de cette surcharge.

Dans ce cas, chaque fois que Python verra _FOR_ITER_ (le sommet de la boucle), il "prédira" que _STORE_FAST_ est le prochain opcode à exécuter. Python jette un œil sur l'opcode suivant et, si la prédiction est correcte, il passe directement à _STORE_FAST_. Cela a pour effet de comprimer les deux opcodes en un seul opcode.

D'autre part, l'opcode _STORE_NAME_ est utilisé dans la boucle au niveau global. Python ne fait * pas * des prédictions similaires lorsqu'il voit cet opcode. Au lieu de cela, il doit remonter au sommet de la boucle d'évaluation, ce qui a des implications évidentes sur la vitesse d'exécution de la boucle.

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

Certains opcodes ont tendance à venir par paires, ce qui permet de prédire le second code lorsque le premier est exécuté. Par exemple, _GET_ITER_ est souvent suivi de _FOR_ITER_. Et FOR_ITER_ EST SOUVENT SUIVI DE _STORE_FAST ou _UNPACK_SEQUENCE_.

La vérification de la prévision coûte un seul test à grande vitesse d'une variable de registre par rapport à une constante. Si l'appariement était bon, les chances de succès de la prédication de branche interne du processeur sont élevées, ce qui entraîne une transition presque nulle vers le code d'opération suivant. Une prédiction réussie enregistre un trajet dans la boucle eval, y compris ses deux branches imprévisibles, le test _HAS_ARG_ et le boîtier de commutation. Combiné à la prédiction de branche interne du processeur, une PREDICT réussie a pour effet de faire en sorte que les deux opcodes s'exécutent comme s'il s'agissait d'un nouvel opcode unique combinant les corps.

Nous pouvons voir dans le code source de l’opcode FOR_ITER 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 se développe en if (*next_instr == op) goto PRED_##op, c’est-à-dire qu’on saute 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 prêt à être exécuté. Python continue dans l'itérable jusqu'à la fin, en effectuant la prédiction réussie à chaque fois.

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

38
Alex Riley