it-swarm.com.de

Warum läuft Python Code in einer Funktion schneller?

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 sich die for-Schleife jedoch nicht in einer Funktion befindet,

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?

796
thedoctar

Sie könnten 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. Beim Kompilieren einer Funktion werden die lokalen Variablen in einem Array mit fester Größe ( und nicht einem dict) gespeichert und Variablennamen zugewiesen zu Indizes. Dies ist möglich, weil Sie einer Funktion keine lokalen Variablen dynamisch hinzufügen können. Das Abrufen einer lokalen Variablen ist dann buchstäblich eine Zeigersuche in der Liste und eine Erhöhung des Nachzählungsbetrags für das PyObject, was trivial ist.

Vergleichen Sie dies mit einer globalen Suche (LOAD_GLOBAL), das ist eine echte dict Suche mit einem Hash und so weiter. Aus diesem Grund müssen Sie übrigens global i Wenn Sie möchten, dass es global ist: Wenn Sie jemals eine Variable innerhalb eines Bereichs zuweisen, gibt der Compiler STORE_FASTs für den Zugriff, es sei denn, Sie weisen es nicht an.

Übrigens sind globale Suchvorgänge immer noch ziemlich optimiert. Attributsuche foo.bar sind die wirklich langsamen!

Hier ist ein kleines Abbildung für die Effizienz lokaler Variablen.

501
Katriel

Innerhalb einer Funktion ist der Bytecode

  2           0 SETUP_LOOP              20 (to 23)
              3 LOAD_GLOBAL              0 (xrange)
              6 LOAD_CONST               3 (100000000)
              9 CALL_FUNCTION            1
             12 GET_ITER            
        >>   13 FOR_ITER                 6 (to 22)
             16 STORE_FAST               0 (i)

  3          19 JUMP_ABSOLUTE           13
        >>   22 POP_BLOCK           
        >>   23 LOAD_CONST               0 (None)
             26 RETURN_VALUE        

Auf der obersten Ebene ist der Bytecode

  1           0 SETUP_LOOP              20 (to 23)
              3 LOAD_NAME                0 (xrange)
              6 LOAD_CONST               3 (100000000)
              9 CALL_FUNCTION            1
             12 GET_ITER            
        >>   13 FOR_ITER                 6 (to 22)
             16 STORE_NAME               1 (i)

  2          19 JUMP_ABSOLUTE           13
        >>   22 POP_BLOCK           
        >>   23 LOAD_CONST               2 (None)
             26 RETURN_VALUE        

Der Unterschied ist, dass STORE_FAST ist schneller (!) als STORE_NAME . Dies liegt daran, dass i in einer Funktion lokal ist, in der obersten Ebene jedoch global.

Verwenden Sie zum Überprüfen des Bytecodes das dis -Modul . Ich konnte die Funktion direkt zerlegen, aber um den Toplevel-Code zu zerlegen, musste ich das compile builtin verwenden.

651
ecatmur

Abgesehen von lokalen/globalen variablen Speicherzeiten beschleunigt die Opcode-Vorhersage die Funktion.

Wie die anderen Antworten erklären, verwendet die Funktion den Opcode STORE_FAST In der Schleife. Hier ist der Bytecode für die Schleife der Funktion:

    >>   13 FOR_ITER                 6 (to 22)   # get next value from iterator
         16 STORE_FAST               0 (x)       # set local variable
         19 JUMP_ABSOLUTE           13           # back to FOR_ITER

Wenn ein Programm ausgeführt wird, führt Python nacheinander jeden Opcode aus, verfolgt den Stapel und führt nach Ausführung jedes Opcodes weitere Überprüfungen des Stapelrahmens durch. Opcode-Vorhersage bedeutet, dass in In bestimmten Fällen kann Python direkt zum nächsten Opcode springen, wodurch ein Teil dieses Overheads vermieden wird.

In diesem Fall wird jedes Mal, wenn Python sieht FOR_ITER (Oben in der Schleife), "vorausgesagt", dass STORE_FAST Der nächste Opcode ist, den es zu haben gilt execute. Python schaut sich dann den nächsten Opcode an und springt, wenn die Vorhersage korrekt war, direkt zu STORE_FAST. Dadurch werden die beiden Opcodes zu einem einzigen Opcode zusammengefasst .

Andererseits wird der Opcode STORE_NAME In der Schleife auf globaler Ebene verwendet. Python macht * nicht * ähnliche Vorhersagen, wenn dieser Opcode angezeigt wird, sondern muss nach oben zurückkehren der Auswerteschleife, die offensichtliche Auswirkungen auf die Geschwindigkeit hat, mit der die Schleife ausgeführt wird.

Um mehr technische Details zu dieser Optimierung zu geben, hier ein Zitat aus der Datei ceval.c (die "Engine" der virtuellen Maschine von Python):

Einige Opcodes kommen in der Regel paarweise, sodass der zweite Code vorhergesagt werden kann, wenn der erste ausgeführt wird. Beispielsweise folgt auf GET_ITER Häufig FOR_ITER. Und FOR_ITER Wird oft gefolgt von STORE_FAST oder UNPACK_SEQUENCE.

Das Überprüfen der Vorhersage kostet einen einzelnen Hochgeschwindigkeitstest einer Registervariable gegen eine Konstante. Wenn das Pairing gut war, besteht eine hohe Wahrscheinlichkeit für den Erfolg der prozessoreigenen Prädikation für interne Verzweigungen, was zu einem nahezu Null-Overhead-Übergang zum nächsten Opcode führt. Eine erfolgreiche Vorhersage erspart eine Reise durch die Auswertungsschleife mit ihren zwei unvorhersehbaren Zweigen, dem HAS_ARG - Test und dem Schalterfall. In Kombination mit der internen Verzweigungsvorhersage des Prozessors bewirkt ein erfolgreiches PREDICT, dass die beiden Opcodes so ausgeführt werden, als wären sie ein einziger neuer Opcode mit den kombinierten Körpern.

Wir können im Quellcode für den FOR_ITER Opcode genau sehen, wo die Vorhersage für STORE_FAST Gemacht wird:

case FOR_ITER:                         // the FOR_ITER opcode case
    v = TOP();
    x = (*v->ob_type->tp_iternext)(v); // x is the next value from iterator
    if (x != NULL) {                     
        Push(x);                       // put x on top of the stack
        PREDICT(STORE_FAST);           // predict STORE_FAST will follow - success!
        PREDICT(UNPACK_SEQUENCE);      // this and everything below is skipped
        continue;
    }
    // error-checking and more code for when the iterator ends normally                                     

Die Funktion PREDICT wird zu if (*next_instr == op) goto PRED_##op erweitert, d. H. Wir springen gerade zum Anfang des vorhergesagten Opcodes. In diesem Fall springen wir hier:

PREDICTED_WITH_ARG(STORE_FAST);
case STORE_FAST:
    v = POP();                     // pop x back off the stack
    SETLOCAL(oparg, v);            // set it as the new local variable
    goto fast_next_opcode;

Die lokale Variable ist nun gesetzt und der nächste Opcode steht zur Ausführung bereit. Python durchläuft die Iteration bis zum Ende und erstellt jedes Mal die erfolgreiche Vorhersage.

Die Python-Wiki-Seite enthält weitere Informationen zur Funktionsweise der virtuellen Maschine von CPython.

38
Alex Riley