En dehors des temps de stockage variables locaux/globaux, prédiction des opcodes rend la fonction plus rapide.
Comme l'expliquent les autres réponses, la fonction utilise la fonction STORE_FAST
dans la boucle. Voici le bytecode de 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 opcode l'un aprÃ?s l'autre, en gardant la trace de la pile et en effectuant d'autres vérifications sur la trame de la pile aprÃ?s l'exécution de chaque opcode. La prédiction d'opcode signifie que dans certains cas, Python est capable de sauter 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 "prédira" que STORE_FAST
est le prochain opcode qu'il doit exécuter. Python jette alors un coup d'œil au prochain opcode 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, le STORE_NAME
L'opcode est utilisé dans la boucle au niveau global. Python fait *pas* faire des prédictions similaires lorsqu'il voit cet opcode. Au lieu de cela, il doit retourner au début de la boucle d'évaluation, ce qui a des implications évidentes sur la vitesse d'exécution de la boucle.
Pour donner un peu plus de détails techniques sur cette optimisation, voici une citation de l'étude de la Commission européenne sur l'optimisation de l'efficacité énergétique. 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 par FOR_ITER
. Et FOR_ITER
est souvent suivi de STORE_FAST
ou UNPACK_SEQUENCE
.
La vérification de la prédiction coûte un seul test à haute vitesse d'un registre par rapport à une constante. Si l'appariement est bon, alors la prédiction de branchement interne du processeur a de fortes chances de réussir. succès, ce qui se traduit par une transition presque sans frais généraux vers l'opcode code opérationnel suivant. Une prédiction réussie permet d'éviter un passage par la boucle d'évaluation y compris ses deux branches imprévisibles, la fonction HAS_ARG
test et le switch-case. Combiné avec la prédiction de branchement interne du processeur, un test réussi PREDICT
a pour effet de faire fonctionner les deux opcodes comme si comme s'il s'agissait d'un seul nouveau code avec les corps combinés.
Nous pouvons voir dans le code source de la FOR_ITER
exactement là où la prédiction de 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
Le site PREDICT
La fonction s'étend à if (*next_instr == op) goto PRED_##op
c'est-à-dire que nous sautons 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 prêt à être exécuté. Python continue à parcourir l'itérable jusqu'à ce qu'il atteigne la fin, en effectuant à chaque fois la prédiction réussie.
Le site Page wiki Python a plus d'informations sur le fonctionnement de la machine virtuelle de CPython.
18 votes
Comment avez-vous fait le chronométrage ?
0 votes
Comportement confirmé pour Python 3.2.3 REPL. Intéressant.
56 votes
C'est juste une intuition, je ne suis pas sûr que ce soit vrai : je pense que c'est à cause des scopes. Dans le cas d'une fonction, une nouvelle portée est créée (c'est-à-dire une sorte de hachage avec des noms de variables liés à leur valeur). Sans fonction, les variables sont dans la portée globale, où vous pouvez trouver beaucoup de choses, ce qui ralentit la boucle.
0 votes
Je ne te croyais pas jusqu'à ce que je reproduise ceci.
Python 2.7.2 (default, Jun 12 2011, 15:08:59) [MSC v.1500 32 bit (Intel)] on win32
5 votes
@Scharron Cela ne semble pas être le cas. J'ai défini 200 000 variables fictives dans la portée sans que cela n'affecte visiblement le temps d'exécution.
0 votes
Intéressant ... je l'ai également reproduit avec Python 2.7.2 sur OSX Lion. 2,2 secondes contre 4,2 secondes.
2 votes
Alex Martelli a écrit une bonne réponse à ce sujet. stackoverflow.com/a/1813167/174728
61 votes
@Scharron vous avez à moitié raison. Il s'agit de scopes, mais la raison pour laquelle c'est plus rapide en local est que les scopes locaux sont en fait implémentés comme des tableaux au lieu de dictionnaires (puisque leur taille est connue à la compilation).
4 votes
@AndrewJaffe La sortie suggère "linux".
time
commandement.0 votes
@AndrewJaffe Ward Muylaert a raison, j'ai utilisé la commande time dans BASH. J'ai maintenant inclus ce détail supplémentaire dans la question.
1 votes
Je viens de tester ce snippet dans IPython 2.7.5 %timeit "def main() : for i in xrange(10 8) : pass ; main()" => 100000000 boucles, best of 3 : 16.9 ns par boucle # et %timeit "for i in xrange(10 8) : pass" => 100000000 boucles, meilleur de 3 : 16.6 ns par boucle