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?
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_FAST
s 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.
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é .
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
_. EtFOR_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, unePREDICT
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.