79 votes

Pourquoi est-code à l'aide de variables intermédiaires plus rapide que le code sans?

J'ai rencontré ce comportement bizarre et pas réussi à l'expliquer. Ces sont les points de repère:

py -3 -m timeit "tuple(range(2000)) == tuple(range(2000))"
10000 loops, best of 3: 97.7 usec per loop
py -3 -m timeit "a = tuple(range(2000));  b = tuple(range(2000)); a==b"
10000 loops, best of 3: 70.7 usec per loop

Comment se fait la comparaison avec une affectation de variable est plus rapide que d'utiliser une seule ligne avec les variables temporaires de plus de 27%?

Par le Python docs, la collecte des ordures est désactivée pendant la timeit donc il ne peut pas l'être. Est-il une sorte d'optimisation?

Les résultats peuvent aussi être reproduits en Python 2.x mais à moindre mesure.

Fonctionnant sous Windows 7, Disponible 3.5.1, Intel i7 cadencé à 3.40 GHz, 64 bits OS et Python. Semble comme une machine différente, j'ai essayé de courir chez Intel i7 3.60 GHz avec Python 3.5.0 ne pas reproduire les résultats.


De course en utilisant la même Python processus avec timeit.timeit() @ 10000 boucles produit 0.703 et 0.804 respectivement. Encore montre bien qu'à un moindre degré. (~12.5%)

113voto

Antti Haapala Points 11542

Mes résultats étaient similaires à la vôtre: le code à l'aide de variables intermédiaires a été assez systématiquement au moins 10 à 20 % plus rapide dans le Python 3.4 que je fatigue. Cependant lorsque je l'ai utilisé IPython sur le même Python 3.4 interprète, j'ai obtenu ces résultats:

In [1]: %timeit -n10000 -r20 tuple(range(2000)) == tuple(range(2000))
10000 loops, best of 20: 74.2 µs per loop

In [2]: %timeit -n10000 -r20 a = tuple(range(2000));  b = tuple(range(2000)); a==b
10000 loops, best of 20: 75.7 µs per loop

Notamment, je n'ai jamais réussi à obtenir, même à proximité de la 74.2 µs pour les anciens quand j'ai utilisé -mtimeit à partir de la ligne de commande.

Donc, ce Heisenbug s'est avéré être quelque chose de très intéressant. J'ai décidé de lancer la commande avec strace et en effet il y a quelque chose de louche se passe:

% strace -o withoutvars python3 -m timeit "tuple(range(2000)) == tuple(range(2000))"
10000 loops, best of 3: 134 usec per loop
% strace -o withvars python3 -mtimeit "a = tuple(range(2000));  b = tuple(range(2000)); a==b"
10000 loops, best of 3: 75.8 usec per loop
% grep mmap withvars|wc -l
46
% grep mmap withoutvars|wc -l
41149

Maintenant que c'est une bonne raison pour la différence. Le code qui n'utilise pas de variables provoque l' mmap système d'appel appelée à peu près 1000x plus que celui qui utilise des variables intermédiaires.

L' withoutvars est plein d' mmap/munmap pour un 256k région; ces mêmes lignes sont répétées encore et encore:

mmap(NULL, 262144, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f32e56de000
munmap(0x7f32e56de000, 262144)          = 0
mmap(NULL, 262144, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f32e56de000
munmap(0x7f32e56de000, 262144)          = 0
mmap(NULL, 262144, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f32e56de000
munmap(0x7f32e56de000, 262144)          = 0

L' mmap appel semble être à venir à partir de la fonction _PyObject_ArenaMmap de Objects/obmalloc.c; obmalloc.c contient aussi de la macro ARENA_SIZE, ce qui est #defined à (256 << 10) (c'est - 262144); de même, l' munmap correspond à l' _PyObject_ArenaMunmap de obmalloc.c.

obmalloc.c dit que

Avant de Python 2.5, les arènes n'ont jamais été free()"ed. Commencer avec Python 2.5, nous essayons de l' free() des arènes, et d'utiliser certaines doux heuristique des stratégies pour augmenter la probabilité que les arènes, éventuellement, peut être libéré.

Ainsi, ces heuristiques et le fait que l'objet Python allocateur de communiqués de ces tribunes libres dès qu'ils sont vidés entraîner python3 -mtimeit 'tuple(range(2000)) == tuple(range(2000))' le déclenchement de comportement pathologique où l'on 256 kio zone de mémoire sont alloués et publié à plusieurs reprises; et cette répartition se passe avec mmap/munmap, ce qui est relativement coûteux car ils sont des appels système - en outre, mmap avec MAP_ANONYMOUS nécessite que le nouveau cartographié les pages doivent être mis à zéro - bien que Python ne serait pas de soins.

Le comportement n'est pas présent dans le code qui utilise les variables intermédiaires, parce que c'est en utilisant un peu plus de mémoire et pas de mémoire arena peut être libéré que certains objets sont toujours attribuées. C'est parce qu' timeit feront une boucle non pas à la différence de

for n in range(10000)
    a = tuple(range(2000))
    b = tuple(range(2000))
    a == b

Maintenant, le comportement est que les deux a et b restera lié jusqu'à ce qu'ils sont réaffectés, donc, dans la deuxième itération, tuple(range(2000)) va allouer un 3ème tuple, et la cession a = tuple(...) va diminuer le nombre de références de l'ancien tuple, à l'origine pour être libéré, et d'augmenter le nombre de références du nouveau tuple; ensuite, la même chose arrive à l' b. Donc après la première itération il y a toujours au moins 2 de ces n-uplets, si ce n'est 3, donc la dérouillée ne se produit pas.

Notamment il ne peut être garanti que le code à l'aide de variables intermédiaires est toujours plus rapide - en effet, dans certaines configurations, il se pourrait qu'à l'aide de variables intermédiaires entraînera extra - mmap des appels, tandis que le code qui compare les valeurs de retour directement peut-être bien.


Quelqu'un a demandé pourquoi cela arrive, quand timeit désactive la collecte des ordures. Il est vrai qu' timeit t-il:

Note

Par défaut, timeit() permet de désactiver temporairement la collecte des ordures pendant le chronométrage. L'avantage de cette approche est qu'il rend indépendant des timings plus comparables. Ce désavantage est que la GC peut être une composante importante de la performance de la fonction en cours de mesure. Si oui, GC peut être activé de nouveau comme la première instruction dans le programme d'installation de chaîne de caractères. Par exemple:

Cependant, le garbage collector de Python n'est là que pour récupérer cyclique des ordures, c'est à dire des collections d'objets dont les références forme de cycles. Il n'est pas le cas ici; au contraire, ces objets sont libérés immédiatement lorsque le compteur de référence tombe à zéro.

7voto

Duncan Points 25356

La première question qui se pose ici est: est-il reproduit? Pour certains d'entre nous au moins c'est bien que d'autres gens disent qu'ils ne sont pas en voir l'effet. Ce sur Fedora, avec le test d'égalité changé d' is que le fait de faire une comparaison ne semble pas pertinent pour le résultat, et la gamme poussé jusqu'à 200 000 comme cela semble pour en maximiser l'effet:

$ python3 -m timeit "a = tuple(range(200000));  b = tuple(range(200000)); a is b"
100 loops, best of 3: 7.03 msec per loop
$ python3 -m timeit "a = tuple(range(200000)) is tuple(range(200000))"
100 loops, best of 3: 10.2 msec per loop
$ python3 -m timeit "tuple(range(200000)) is tuple(range(200000))"
100 loops, best of 3: 10.2 msec per loop
$ python3 -m timeit "a = b = tuple(range(200000)) is tuple(range(200000))"
100 loops, best of 3: 9.99 msec per loop
$ python3 -m timeit "a = b = tuple(range(200000)) is tuple(range(200000))"
100 loops, best of 3: 10.2 msec per loop
$ python3 -m timeit "tuple(range(200000)) is tuple(range(200000))"
100 loops, best of 3: 10.1 msec per loop
$ python3 -m timeit "a = tuple(range(200000));  b = tuple(range(200000)); a is b"
100 loops, best of 3: 7 msec per loop
$ python3 -m timeit "a = tuple(range(200000));  b = tuple(range(200000)); a is b"
100 loops, best of 3: 7.02 msec per loop

Je remarque que les variations entre les pistes, et l'ordre dans lequel les expressions sont run, très peu de différence pour le résultat.

L'ajout d'affectations d' a et b dans la version lente de ne pas accélérer. En fait, comme on peut l'attendre de l'affectation à des variables locales a un effet négligeable. La seule chose qui ne de vitesse, il est le fractionnement de l'expression entièrement en deux. La seule différence que cela devrait être fait est qu'il réduit au maximum la profondeur de la pile utilisée par Python, tandis que l'évaluation de l'expression (4 à 3).

Cela nous donne l'idée que l'effet est lié à la pile de profondeur, peut-être le niveau supplémentaire pousse la pile à travers dans une autre page de mémoire. Si donc, nous devrions voir, que faire d'autres changements qui affectent la pile va changer (plus de chances de tuer l'effet), et en fait, c'est ce que nous voyons:

$ python3 -m timeit -s "def foo():
   tuple(range(200000)) is tuple(range(200000))" "foo()"
100 loops, best of 3: 10 msec per loop
$ python3 -m timeit -s "def foo():
   tuple(range(200000)) is tuple(range(200000))" "foo()"
100 loops, best of 3: 10 msec per loop
$ python3 -m timeit -s "def foo():
   a = tuple(range(200000));  b = tuple(range(200000)); a is b" "foo()"
100 loops, best of 3: 9.97 msec per loop
$ python3 -m timeit -s "def foo():
   a = tuple(range(200000));  b = tuple(range(200000)); a is b" "foo()"
100 loops, best of 3: 10 msec per loop

Donc, je pense que l'effet est entièrement due à la façon dont beaucoup de Python pile est consommée lors de la synchronisation des processus. C'est toujours bizarre.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X