Mis à part les temps de stockage variables locaux / globaux, la prédiction d'opcode rend la fonction plus rapide.
Comme les autres réponses l'expliquent, la fonction utilise l' 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 opcode l'un après l'autre, en gardant une trace de la pile et en effectuant d'autres vérifications sur le cadre 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 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 "prédira" que STORE_FAST
c'est le prochain opcode qu'il devra exécuter. Python jette ensuite un œil à l'opcode suivant et, si la prédiction était correcte, il passe directement à STORE_FAST
. Cela a pour effet de compresser les deux opcodes en un seul opcode.
En revanche, l' STORE_NAME
opcode est utilisé dans la boucle au niveau global. Python ne fait * pas * de prédictions similaires lorsqu'il voit cet opcode. Au lieu de cela, il doit remonter en haut 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 du ceval.c
fichier (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 deSTORE_FAST
ou UNPACK_SEQUENCE
.
La vérification de la prédiction coûte un seul test à grande vitesse d'une variable de registre par rapport à une constante. Si le couplage était bon, alors la propre prédiction de branche interne du processeur a une forte probabilité de succès, ce qui entraîne une transition presque nulle pour le prochain opcode. Une prédiction réussie enregistre un voyage à travers la boucle d'évaluation, y compris ses deux branches imprévisibles, le HAS_ARG
test et le boîtier de commutation. Combiné avec la prédiction de branche interne du processeur, un succès PREDICT
a pour effet de faire fonctionner les deux opcodes comme s'ils étaient un seul nouvel opcode avec les corps combinés.
Nous pouvons voir dans le code source de l' FOR_ITER
opcode exactement où la prédiction 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 PREDICT
fonction se développe pour if (*next_instr == op) goto PRED_##op
dire que nous venons de sauter 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 à travers l'itérable jusqu'à ce qu'il atteigne la fin, faisant la prédiction réussie à chaque fois.
La page wiki Python contient plus d'informations sur le fonctionnement de la machine virtuelle de CPython.