def main():
for i in xrange(10**8):
pass
main()
Ce morceau de code en Python s'exécute dans (Remarque: Le timing est fait avec la fonction de temps 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
puis il fonctionne beaucoup plus longtemps:
real 0m4.543s
user 0m4.524s
sys 0m0.012s
Pourquoi est-ce?
python
performance
profiling
benchmarking
cpython
thedoctar
la source
la source
Réponses:
Vous vous demandez peut-être pourquoi il est plus rapide de stocker des variables locales que des variables globales. Il s'agit d'un détail d'implémentation CPython.
N'oubliez pas 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 ( pas a
dict
) et des noms de variables sont attribués aux index. Cela est possible car vous ne pouvez pas ajouter dynamiquement 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 cePyObject
qui est trivial.Comparez cela à une recherche globale (
LOAD_GLOBAL
), qui est une véritabledict
recherche impliquant un hachage et ainsi de suite. Soit dit en passant, c'est pourquoi vous devez spécifierglobal i
si vous voulez qu'elle soit globale: si vous attribuez jamais à une variable à l'intérieur d'une portée, le compilateur émettra desSTORE_FAST
s pour son accès, sauf si vous lui dites de ne pas le faire.Soit dit en passant, les recherches globales sont encore assez optimisées. Les recherches d'attributs
foo.bar
sont les plus lentes!Voici une petite illustration de l'efficacité variable locale.
la source
def foo_func: x = 5
,x
est local à une fonction. L'accèsx
est local.foo = SomeClass()
,foo.bar
est l'accès aux attributs.val = 5
global est global. Quant à l'attribut speed local> global> selon ce que j'ai lu ici. Ainsi , l' accèsx
àfoo_func
est le plus rapide, suivival
, suivifoo.bar
.foo.attr
n'est pas une recherche locale car dans le contexte de ce convo, nous parlons de recherches locales comme une recherche d'une variable qui appartient à une fonction.globals()
fonction. Si vous voulez plus d'informations que cela, vous devrez peut-être commencer à regarder le code source de Python. Et CPython n'est que le nom de l'implémentation habituelle de Python - vous l'utilisez probablement déjà!A l'intérieur d'une fonction, le bytecode est:
Au niveau supérieur, le bytecode est:
La différence est que
STORE_FAST
c'est plus rapide (!) QueSTORE_NAME
. C'est parce que dans une fonction,i
c'est un local mais au niveau supérieur c'est un global.Pour examiner le bytecode, utilisez le
dis
module . J'ai pu démonter la fonction directement, mais pour démonter le code de haut niveau, j'ai dû utiliser la fonctioncompile
intégrée .la source
global i
dans lamain
fonction rend les temps d'exécution équivalents.locals()
, ouinspect.getframe()
etc.). La recherche d'un élément de tableau par un entier constant est beaucoup plus rapide que la recherche d'un dict.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: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" queSTORE_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):Nous pouvons voir dans le code source de l'
FOR_ITER
opcode exactement où la prédictionSTORE_FAST
est faite:La
PREDICT
fonction se développe pourif (*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: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.
la source
HAS_ARG
test ne se produit jamais (sauf lorsque le traçage de bas niveau est activé à la fois lors de la compilation et de l'exécution, ce qui n'est pas le cas pour une construction normale), ne laissant qu'un seul saut imprévisible.PREDICT
macro est complètement désactivée; à la place, la plupart des cas se terminent par unDISPATCH
branchement direct. Mais sur les CPU de prédiction de branche, l'effet est similaire à celui dePREDICT
, puisque la branche (et la prédiction) est par opcode, augmentant les chances de prédiction de branche réussie.