def main():
for i in xrange(10**8):
pass
main()
Dieser Code in Python läuft in (Hinweis: Das Timing erfolgt mit der Zeitfunktion in BASH unter Linux.)
real 0m1.841s
user 0m1.828s
sys 0m0.012s
Wenn die for-Schleife jedoch nicht in einer Funktion platziert ist,
for i in xrange(10**8):
pass
dann läuft es viel länger:
real 0m4.543s
user 0m4.524s
sys 0m0.012s
Warum ist das?
python
performance
profiling
benchmarking
cpython
Thedoctar
quelle
quelle
Antworten:
Sie könnten sich fragen, warum es schneller ist, lokale Variablen als globale zu speichern. Dies ist ein CPython-Implementierungsdetail.
Denken Sie daran, dass CPython zu Bytecode kompiliert wird, den der Interpreter ausführt. Wenn eine Funktion kompiliert wird, werden die lokalen Variablen in einem Array fester Größe ( nicht a
dict
) gespeichert und den Indizes Variablennamen zugewiesen. Dies ist möglich, weil Sie einer Funktion keine dynamischen Variablen dynamisch hinzufügen können. Das Abrufen einer lokalen Variablen ist dann buchstäblich eine Zeigersuche in der Liste und eine Erhöhung der Anzahl der Nachzähler,PyObject
was trivial ist.Vergleichen Sie dies mit einem globalen Lookup (
LOAD_GLOBAL
), bei dem es sich um eine echtedict
Suche mit einem Hash usw. handelt. Aus diesem Grund müssen Sie im Übrigen angeben,global i
ob es global sein soll: Wenn Sie jemals eine Variable innerhalb eines Bereichs zuweisen, gibt der CompilerSTORE_FAST
s für den Zugriff aus, es sei denn, Sie weisen ihn an , dies nicht zu tun.Übrigens sind globale Suchvorgänge immer noch ziemlich optimiert. Attributsuchen
foo.bar
sind die wirklich langsamen!Hier ist eine kleine Illustration zur lokalen variablen Effizienz.
quelle
def foo_func: x = 5
,x
ist lokal für eine Funktion. Der Zugriffx
erfolgt lokal.foo = SomeClass()
,foo.bar
Ist Attribut Zugriff.val = 5
global ist global. Bezüglich des Attributs speed local> global> gemäß dem, was ich hier gelesen habe. So Zugriffx
infoo_func
ist am schnellsten, gefolgt vonval
, gefolgt vonfoo.bar
.foo.attr
ist keine lokale Suche, da es sich im Kontext dieser Convo um eine lokale Suche handelt, bei der es sich um eine Suche nach einer Variablen handelt, die zu einer Funktion gehört.globals()
Funktion. Wenn Sie mehr Informationen wünschen, müssen Sie möglicherweise den Quellcode für Python suchen. Und CPython ist nur der Name für die übliche Implementierung von Python - Sie verwenden es also wahrscheinlich bereits!Innerhalb einer Funktion lautet der Bytecode:
Auf der obersten Ebene lautet der Bytecode:
Der Unterschied ist, dass
STORE_FAST
es schneller (!) Ist alsSTORE_NAME
. Dies liegt daran, dass eine Funktioni
lokal ist, auf oberster Ebene jedoch global.Verwenden Sie das
dis
Modul , um den Bytecode zu überprüfen . Ich konnte die Funktion direkt zerlegen, aber um den obersten Code zu zerlegen, musste ich dencompile
eingebauten verwenden .quelle
global i
in diemain
Funktion werden die Laufzeiten gleichwertig.locals()
oderinspect.getframe()
usw.). Das Nachschlagen eines Array-Elements mit einer konstanten Ganzzahl ist viel schneller als das Durchsuchen eines Diktats.Abgesehen von lokalen / globalen variablen Speicherzeiten beschleunigt die Opcode-Vorhersage die Funktion.
Wie die anderen Antworten erklären, verwendet die Funktion den
STORE_FAST
Opcode in der Schleife. Hier ist der Bytecode für die Funktionsschleife:Normalerweise führt Python beim Ausführen eines Programms jeden Opcode nacheinander aus, verfolgt den Stapel und führt nach Ausführung jedes Opcodes weitere Überprüfungen des Stapelrahmens durch. Opcode-Vorhersage bedeutet, dass Python in bestimmten Fällen direkt zum nächsten Opcode springen kann, wodurch ein Teil dieses Overheads vermieden wird.
In diesem Fall
FOR_ITER
"sagt" Python jedes Mal, wenn es den oberen Rand der Schleife sieht , voraus, dass diesSTORE_FAST
der nächste Opcode ist, den es ausführen muss. Python schaut dann auf den nächsten Opcode und springt, wenn die Vorhersage korrekt war, direkt zuSTORE_FAST
. Dies hat den Effekt, dass die beiden Opcodes zu einem einzigen Opcode zusammengefasst werden.Auf der anderen Seite die
STORE_NAME
Opcode in der Schleife auf globaler Ebene verwendet. Python macht * keine * ähnlichen Vorhersagen, wenn es diesen Opcode sieht. Stattdessen muss es zum Anfang der Auswertungsschleife zurückkehren, was offensichtliche Auswirkungen auf die Geschwindigkeit hat, mit der die Schleife ausgeführt wird.Um weitere technische Details zu dieser Optimierung zu erhalten, finden Sie hier ein Zitat aus dem
ceval.c
Datei (der "Engine" der virtuellen Maschine von Python):Wir können im Quellcode für den
FOR_ITER
Opcode genau sehen, wo die Vorhersage fürSTORE_FAST
gemacht wird:Die
PREDICT
Funktion wird erweitert aufif (*next_instr == op) goto PRED_##op
dh wir springen einfach zum Anfang des vorhergesagten Opcodes. In diesem Fall springen wir hier:Die lokale Variable ist jetzt gesetzt und der nächste Opcode steht zur Ausführung bereit. Python fährt mit dem Iterable fort, bis es das Ende erreicht, und macht jedes Mal die erfolgreiche Vorhersage.
Auf der Python-Wiki-Seite finden Sie weitere Informationen zur Funktionsweise der virtuellen Maschine von CPython.
quelle
HAS_ARG
Test nie durchgeführt (außer wenn die Ablaufverfolgung auf niedriger Ebene sowohl zur Kompilierung als auch zur Laufzeit aktiviert ist, was bei keinem normalen Build der Fall ist), sodass nur ein unvorhersehbarer Sprung verbleibt.PREDICT
Makro vollständig deaktiviert. Stattdessen enden die meisten Fälle in einemDISPATCH
, der direkt verzweigt. Bei Verzweigungsvorhersage-CPUs ist der Effekt ähnlich dem vonPREDICT
, da die Verzweigung (und Vorhersage) pro Opcode erfolgt, was die Wahrscheinlichkeit einer erfolgreichen Verzweigungsvorhersage erhöht.