Meine Ergebnisse waren ähnlich wie Ihre: Der Code mit Zwischenvariablen war in Python 3.4 ziemlich konsistent mindestens 10-20% schneller. Wenn ich jedoch IPython auf demselben Python 3.4-Interpreter verwendet habe, habe ich folgende Ergebnisse erhalten:
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
Insbesondere habe ich es nie geschafft, die 74,2 µs für die ersteren zu erreichen, als ich sie -mtimeit
über die Befehlszeile verwendet habe.
Dieser Heisenbug erwies sich also als etwas ziemlich Interessantes. Ich habe beschlossen, den Befehl mit auszuführen, strace
und tatsächlich ist etwas faul:
% 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
Das ist ein guter Grund für den Unterschied. Der Code, der keine Variablen verwendet, bewirkt, dass der mmap
Systemaufruf fast 1000x häufiger aufgerufen wird als der Code, der Zwischenvariablen verwendet.
Das withoutvars
ist voll von mmap
/ munmap
für eine 256k Region; Dieselben Zeilen werden immer wieder wiederholt:
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
Der mmap
Aufruf scheint von der Funktion _PyObject_ArenaMmap
von zu kommen Objects/obmalloc.c
; das obmalloc.c
enthält auch das Makro ARENA_SIZE
, das #define
d sein soll (256 << 10)
(das heißt 262144
); ähnlich munmap
passt das _PyObject_ArenaMunmap
von obmalloc.c
.
obmalloc.c
sagt, dass
Vor Python 2.5 wurden Arenen nie free()
bearbeitet. Ab Python 2.5 versuchen wir, free()
Arenen zu erstellen, und verwenden einige milde heuristische Strategien, um die Wahrscheinlichkeit zu erhöhen, dass Arenen schließlich freigegeben werden können.
Daher führen diese Heuristiken und die Tatsache, dass der Python-Objektzuweiser diese freien Arenen freigibt, sobald sie geleert sind, zu einem python3 -mtimeit 'tuple(range(2000)) == tuple(range(2000))'
pathologischen Verhalten, bei dem ein Speicherbereich von 256 kiB wiederholt neu zugewiesen und freigegeben wird. und diese Zuordnung geschieht mit mmap
/ munmap
, die vergleichsweise teuer , wie sie sind Systemaufrufe ist - weiterhin mmap
mit MAP_ANONYMOUS
erfordert , dass die neu kartiert Seiten müssen auf Null gesetzt werden - auch wenn Python würde sich nicht darum.
Das Verhalten ist in dem Code, der Zwischenvariablen verwendet, nicht vorhanden, da etwas mehr Speicher verwendet wird und kein Speicherbereich freigegeben werden kann, da einige Objekte noch darin zugeordnet sind. Das liegt daran timeit
, dass es nicht anders als eine Schleife wird
for n in range(10000)
a = tuple(range(2000))
b = tuple(range(2000))
a == b
Das Verhalten ist nun, dass beide a
und b
bleiben gebunden, bis sie * neu zugewiesen werden. In der zweiten Iteration tuple(range(2000))
wird also ein drittes Tupel zugewiesen, und die Zuweisung a = tuple(...)
verringert die Referenzanzahl des alten Tupels, wodurch es freigegeben wird, und erhöht sich der Referenzzähler des neuen Tupels; dann passiert das gleiche mit b
. Daher gibt es nach der ersten Iteration immer mindestens 2 dieser Tupel, wenn nicht 3, so dass das Thrashing nicht auftritt.
Insbesondere kann nicht garantiert werden, dass der Code mit Zwischenvariablen immer schneller ist. In einigen Setups kann die Verwendung von Zwischenvariablen zu zusätzlichen mmap
Aufrufen führen, während der Code, der die Rückgabewerte direkt vergleicht, möglicherweise in Ordnung ist.
Jemand fragte, warum dies passiert, wenn die timeit
Speicherbereinigung deaktiviert wird. Es ist in der Tat wahr, dass es timeit
tut :
Hinweis
Standardmäßig wird timeit()
die Speicherbereinigung während des Timings vorübergehend deaktiviert. Der Vorteil dieses Ansatzes besteht darin, dass unabhängige Timings vergleichbarer werden. Dieser Nachteil besteht darin, dass GC ein wichtiger Bestandteil der Leistung der gemessenen Funktion sein kann. In diesem Fall kann GC als erste Anweisung in der Setup-Zeichenfolge wieder aktiviert werden. Zum Beispiel:
Der Garbage Collector von Python ist jedoch nur dazu da, zyklischen Müll zurückzugewinnen , dh Sammlungen von Objekten, deren Referenzen Zyklen bilden. Dies ist hier nicht der Fall; Stattdessen werden diese Objekte sofort freigegeben, wenn der Referenzzähler auf Null fällt.
dis.dis("tuple(range(2000)) == tuple(range(2000))")
mitdis.dis("a = tuple(range(2000)); b = tuple(range(2000)); a==b")
. In meiner Konfiguration enthält das zweite Snippet tatsächlich den gesamten Bytecode des ersten und einige zusätzliche Anweisungen. Es ist kaum zu glauben, dass mehr Bytecode-Anweisungen zu einer schnelleren Ausführung führen. Vielleicht ist es ein Fehler in einer bestimmten Python-Version?