Perché il codice Python viene eseguito più velocemente in una funzione?


836
def main():
    for i in xrange(10**8):
        pass
main()

Questo pezzo di codice in Python viene eseguito (Nota: il tempismo viene eseguito con la funzione time in BASH in Linux.)

real    0m1.841s
user    0m1.828s
sys     0m0.012s

Tuttavia, se il ciclo for non è inserito in una funzione,

for i in xrange(10**8):
    pass

quindi funziona per un tempo molto più lungo:

real    0m4.543s
user    0m4.524s
sys     0m0.012s

Perchè è questo?


16
Come hai effettivamente fatto i tempi?
Andrew Jaffe,

53
Solo un'intuizione, non sono sicuro che sia vero: immagino che sia per scopi. Nel caso di funzione, viene creato un nuovo ambito (ovvero tipo di hash con nomi di variabili associati al loro valore). Senza una funzione, le variabili sono nell'ambito globale, quando puoi trovare molte cose, quindi rallentando il ciclo.
Scharron,

4
@Scharron Non sembra essere quello. Definite 200k variabili fittizie nell'ambito senza che ciò influisca visibilmente sul tempo di esecuzione.
Deestan,

2
Alex Martelli ha scritto una buona risposta in merito a questo stackoverflow.com/a/1813167/174728
John La Rooy,

53
@Scharron hai quasi ragione. Riguarda gli ambiti, ma la ragione per cui è più veloce nei locali è che gli ambiti locali sono effettivamente implementati come array anziché come dizionari (poiché le loro dimensioni sono note al momento della compilazione).
Katriel,

Risposte:


532

Potresti chiederti perché è più veloce memorizzare variabili locali rispetto ai globali. Questo è un dettaglio dell'implementazione di CPython.

Ricorda che CPython è compilato in bytecode, che viene eseguito dall'interprete. Quando viene compilata una funzione, le variabili locali vengono archiviate in un array di dimensioni fisse ( non a dict) e i nomi delle variabili vengono assegnati agli indici. Ciò è possibile perché non è possibile aggiungere dinamicamente variabili locali a una funzione. Quindi il recupero di una variabile locale è letteralmente una ricerca del puntatore nell'elenco e un aumento del conteggio su PyObjectquale è banale.

Confrontalo con una ricerca globale ( LOAD_GLOBAL), che è una vera dictricerca che coinvolge un hash e così via. Per inciso, questo è il motivo per cui è necessario specificare global ise si desidera che sia globale: se mai si assegna a una variabile all'interno di un ambito, il compilatore emetterà STORE_FASTs per il suo accesso a meno che non gli si dica di non farlo.

A proposito, le ricerche globali sono ancora piuttosto ottimizzate. Le ricerche di attributi foo.barsono davvero lente!

Ecco una piccola illustrazione sull'efficienza delle variabili locali.


6
Questo vale anche per PyPy, fino alla versione corrente (1.8 al momento della stesura di questo documento). Il codice di test dall'OP viene eseguito circa quattro volte più lentamente nell'ambito globale rispetto a una funzione.
GDorn,

4
@Walkerneo Lo sono, a meno che tu non l'abbia detto al contrario. Secondo quanto dicono katrielalex ed ecatmur, le ricerche sulle variabili globali sono più lente delle ricerche sulle variabili locali a causa del metodo di archiviazione.
Jeremy Pridemore,

2
@Walkerneo La conversazione principale in corso qui è il confronto tra ricerche di variabili locali all'interno di una funzione e ricerche di variabili globali definite a livello di modulo. Se noti nel tuo commento originale la risposta a questa risposta, hai detto "Non avrei mai pensato che le ricerche delle variabili globali fossero più veloci delle ricerche delle proprietà delle variabili locali". e non lo sono. katrielalex ha affermato che, sebbene le ricerche delle variabili locali siano più veloci di quelle globali, anche quelle globali sono piuttosto ottimizzate e più veloci delle ricerche di attributi (che sono diverse). Non ho abbastanza spazio in questo commento per di più.
Jeremy Pridemore,

3
@Walkerneo foo.bar non è un accesso locale. È un attributo di un oggetto. (Perdona la mancanza di formattazione) def foo_func: x = 5, xè locale a una funzione. L'accesso xè locale. foo = SomeClass(), foo.barè l'accesso agli attributi. val = 5globale è globale. Per quanto riguarda l'attributo speed> local> globale secondo quanto ho letto qui. Quindi l'accesso xin foo_funcè più veloce, seguito da val, seguito da foo.bar. foo.attrnon è una ricerca locale perché nel contesto di questo convo, stiamo parlando di ricerche locali che sono una ricerca di una variabile che appartiene a una funzione.
Jeremy Pridemore,

3
@thedoctar dai un'occhiata alla globals()funzione. Se desideri più informazioni di quelle, potresti dover iniziare a cercare il codice sorgente per Python. E CPython è solo il nome della solita implementazione di Python - quindi probabilmente lo stai già usando!
Katriel,

661

All'interno di una funzione, il 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        

Al livello superiore, il 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        

La differenza è che STORE_FASTè più veloce (!) Rispetto STORE_NAME. Questo perché in una funzione iè locale ma a livello superiore è globale.

Per esaminare il bytecode, utilizzare il dismodulo . Sono stato in grado di disassemblare direttamente la funzione, ma per smontare il codice di livello superiore ho dovuto usare l' compileintegrato .


171
Confermato da esperimento. L'inserimento global inella mainfunzione rende equivalenti i tempi di funzionamento.
Deestan,

44
Questo risponde alla domanda senza rispondere alla domanda :) Nel caso di variabili di funzione locali, CPython le memorizza effettivamente in una tupla (che è mutabile dal codice C) fino a quando non viene richiesto un dizionario (ad es. Via locals(), o inspect.getframe()ecc.). Cercare un elemento array con un numero intero costante è molto più veloce della ricerca di un dict.
dmw,

3
È lo stesso anche con C / C ++, l'uso di variabili globali provoca un significativo rallentamento
codejammer

3
Questa è la prima volta che ho visto il bytecode. Come si vede, ed è importante saperlo?
Zack,

4
@gkimsey Sono d'accordo. Volevo solo condividere due cose i) Questo comportamento è notato in altri linguaggi di programmazione ii) L'agente causale è più il lato architettonico e non il linguaggio stesso nel vero senso
codejammer

42

A parte i tempi di memorizzazione variabili locali / globali, la previsione del codice operativo rende la funzione più veloce.

Come spiegano le altre risposte, la funzione utilizza il STORE_FASTcodice operativo nel ciclo. Ecco il bytecode per il loop della funzione:

    >>   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

Normalmente quando viene eseguito un programma, Python esegue ogni codice operativo uno dopo l'altro, tenendo traccia dello stack e preformando altri controlli sul frame dello stack dopo l'esecuzione di ciascun codice operativo. La previsione di Opcode significa che in alcuni casi Python è in grado di passare direttamente al successivo opcode, evitando così un certo sovraccarico.

In questo caso, ogni volta che Python vede FOR_ITER(la parte superiore del ciclo), "prevede" STORE_FASTil prossimo opcode che deve eseguire. Python quindi dà un'occhiata al successivo codice operativo e, se la previsione era corretta, passa direttamente a STORE_FAST. Ciò ha l'effetto di spremere i due codici operativi in ​​un unico codice operativo.

D'altra parte, il STORE_NAMEcodice operativo viene utilizzato nel ciclo a livello globale. Python * non * fa previsioni simili quando vede questo codice operativo. Invece, deve tornare all'inizio del ciclo di valutazione che ha ovvie implicazioni per la velocità con cui il ciclo viene eseguito.

Per fornire ulteriori dettagli tecnici su questa ottimizzazione, ecco una citazione dal ceval.cfile (il "motore" della macchina virtuale di Python):

Alcuni codici operativi tendono a venire in coppia, rendendo così possibile prevedere il secondo codice quando viene eseguito il primo. Ad esempio, GET_ITERè spesso seguito da FOR_ITER. Ed FOR_ITERè spesso seguito daSTORE_FAST o UNPACK_SEQUENCE.

La verifica della previsione costa un singolo test ad alta velocità di una variabile di registro rispetto a una costante. Se l'accoppiamento è andato a buon fine, la previsione della diramazione interna del processore ha un'elevata probabilità di successo, con una transizione quasi zero al successivo opcode. Una previsione riuscita salva un viaggio attraverso il circuito di valutazione inclusi i suoi due rami imprevedibili, il HAS_ARGtest e la custodia. In combinazione con la previsione del ramo interno del processore, un successo PREDICTha l'effetto di far funzionare i due codici operativi come se fossero un unico nuovo codice operativo con i corpi combinati.

Possiamo vedere nel codice sorgente per il codice FOR_ITERoperativo esattamente dove STORE_FASTviene fatta la previsione :

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                                     

La PREDICTfunzione si espande, if (*next_instr == op) goto PRED_##opovvero passiamo all'inizio dell'opcode previsto. In questo caso, saltiamo qui:

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;

La variabile locale è ora impostata e il successivo opcode è pronto per l'esecuzione. Python continua attraverso l'iterabile fino a raggiungere la fine, facendo ogni volta la previsione riuscita.

La pagina wiki di Python contiene ulteriori informazioni su come funziona la macchina virtuale di CPython.


Aggiornamento minore: a partire da CPython 3.6, i risparmi dalla previsione diminuiscono un po '; invece di due rami imprevedibili, ce n'è solo uno. La modifica è dovuta al passaggio dal bytecode al wordcode ; ora tutti i "codici di parole" hanno un argomento, è solo a zero quando l'istruzione non accetta logicamente un argomento. Pertanto, il HAS_ARGtest non si verifica mai (tranne quando la traccia di basso livello è abilitata sia in fase di compilazione che in fase di esecuzione, cosa che nessuna build normale fa), lasciando solo un salto imprevedibile.
ShadowRanger

Anche quel salto imprevedibile non accade nella maggior parte delle build di CPython, a causa del nuovo comportamento di goto calcolato (a partire da Python 3.1 , abilitato di default in 3.2 ); quando utilizzata, la PREDICTmacro è completamente disabilitata; invece la maggior parte dei casi termina direttamente in un DISPATCHramo. Ma sulle CPU che prevedono le succursali, l'effetto è simile a quello di PREDICT, poiché la ramificazione (e la previsione) è per codice operativo, aumentando le probabilità di una previsione della succursale riuscita.
ShadowRanger
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.