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 #define
d à (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.