Perché [] è più veloce di list ()?


706

Di recente ho confrontato le velocità di elaborazione di []e sono list()rimasto sorpreso di scoprire che []corre più di tre volte più veloce di list(). Ho eseguito lo stesso test con {}e dict()ei risultati sono stati praticamente identici: []ed {}entrambi sono voluti circa 0.128sec / milioni di cicli, mentre list()e dict()ha preso circa 0.428sec / milioni di cicli ciascuno.

Perchè è questo? Fare []e {}(e probabilmente ()e '', anche) immediatamente passare indietro di copie di alcuni vuoto stock letterale mentre i loro omologhi in modo esplicito con nomi ( list(), dict(), tuple(), str()) andare completamente sulla creazione di un oggetto, indipendentemente dal fatto che in realtà hanno elementi?

Non ho idea di come questi due metodi differiscano, ma mi piacerebbe scoprirlo. Non sono riuscito a trovare una risposta nei documenti o su SO, e cercare parentesi vuote si è rivelato più problematico di quanto mi aspettassi.

Ho ottenuto i miei risultati di temporizzazione chiamando timeit.timeit("[]")e timeit.timeit("list()"), e timeit.timeit("{}")e timeit.timeit("dict()"), per confrontare rispettivamente elenchi e dizionari. Sto eseguendo Python 2.7.9.

Di recente ho scoperto " Perché se Vero più lento se 1? " Che mette a confronto le prestazioni di if Trueper if 1e sembra toccare un simile letterale-versus-global scenario; forse vale la pena considerare anche.


2
Nota: ()e ''sono speciali, poiché non sono solo vuoti, sono immutabili e, in quanto tali, è una vittoria facile renderli singoli; non costruiscono nemmeno nuovi oggetti, caricano solo il singleton per il vuoto tuple/ str. Tecnicamente un dettaglio di implementazione, ma ho difficoltà a immaginare perché non memorizzino nella cache il vuoto tuple/ strper motivi di prestazioni. Quindi la tua intuizione []e il {}ritorno di un titolo letterale erano sbagliati, ma si applica a ()e ''.
ShadowRanger

Risposte:


757

Perché []e {}sono sintassi letterale . Python può creare bytecode solo per creare la lista o gli oggetti del dizionario:

>>> import dis
>>> dis.dis(compile('[]', '', 'eval'))
  1           0 BUILD_LIST               0
              3 RETURN_VALUE        
>>> dis.dis(compile('{}', '', 'eval'))
  1           0 BUILD_MAP                0
              3 RETURN_VALUE        

list()e dict()sono oggetti separati. I loro nomi devono essere risolti, lo stack deve essere coinvolto per inviare gli argomenti, il frame deve essere archiviato per essere recuperato in seguito e deve essere effettuata una chiamata. Tutto ciò richiede più tempo.

Per il caso vuoto, ciò significa che devi almeno un LOAD_NAME(che deve cercare nello spazio dei nomi globale e nel __builtin__modulo ) seguito da un CALL_FUNCTION, che deve preservare il frame corrente:

>>> dis.dis(compile('list()', '', 'eval'))
  1           0 LOAD_NAME                0 (list)
              3 CALL_FUNCTION            0
              6 RETURN_VALUE        
>>> dis.dis(compile('dict()', '', 'eval'))
  1           0 LOAD_NAME                0 (dict)
              3 CALL_FUNCTION            0
              6 RETURN_VALUE        

È possibile cronometrare la ricerca del nome separatamente con timeit:

>>> import timeit
>>> timeit.timeit('list', number=10**7)
0.30749011039733887
>>> timeit.timeit('dict', number=10**7)
0.4215109348297119

La discrepanza temporale è probabilmente una collisione dell'hash del dizionario. Sottrai quei tempi dai tempi per chiamare quegli oggetti e confronta il risultato con i tempi per usare i letterali:

>>> timeit.timeit('[]', number=10**7)
0.30478692054748535
>>> timeit.timeit('{}', number=10**7)
0.31482696533203125
>>> timeit.timeit('list()', number=10**7)
0.9991960525512695
>>> timeit.timeit('dict()', number=10**7)
1.0200958251953125

Quindi dover chiamare l'oggetto richiede ulteriori 1.00 - 0.31 - 0.30 == 0.39secondi per 10 milioni di chiamate.

Puoi evitare il costo di ricerca globale aliasando i nomi globali come locali (usando timeitun'impostazione, tutto ciò che ti lega a un nome è locale):

>>> timeit.timeit('_list', '_list = list', number=10**7)
0.1866450309753418
>>> timeit.timeit('_dict', '_dict = dict', number=10**7)
0.19016098976135254
>>> timeit.timeit('_list()', '_list = list', number=10**7)
0.841480016708374
>>> timeit.timeit('_dict()', '_dict = dict', number=10**7)
0.7233691215515137

ma non puoi mai superare quel CALL_FUNCTIONcosto.


150

list()richiede una ricerca globale e una chiamata di funzione ma si []compila in una singola istruzione. Vedere:

Python 2.7.3
>>> import dis
>>> print dis.dis(lambda: list())
  1           0 LOAD_GLOBAL              0 (list)
              3 CALL_FUNCTION            0
              6 RETURN_VALUE        
None
>>> print dis.dis(lambda: [])
  1           0 BUILD_LIST               0
              3 RETURN_VALUE        
None

75

Perché listè una funzione per convertire dire una stringa in un oggetto elenco, mentre []viene utilizzata per creare un elenco fuori dal pipistrello. Prova questo (potrebbe avere più senso per te):

x = "wham bam"
a = list(x)
>>> a
["w", "h", "a", "m", ...]

Mentre

y = ["wham bam"]
>>> y
["wham bam"]

Ti dà un elenco reale contenente tutto ciò che hai inserito.


7
Questo non affronta direttamente la domanda. La domanda era: perché []è più veloce di list(), non perché ['wham bam']è più veloce di list('wham bam').
Jeremy Visser,

2
@JeremyVisser Questo per me aveva poco senso perché []/ list()è esattamente lo stesso di ['wham']/ list('wham')perché hanno le stesse differenze variabili, così come lo 1000/10sono 100/1in matematica. In teoria si potrebbe togliere wham bame il fatto sarebbe sempre lo stesso, che list()prova a convertire qualcosa chiamando un nome di funzione mentre []convertirà semplicemente la variabile. Le chiamate di funzione sono diverse sì, questa è solo una panoramica logica del problema poiché ad esempio una mappa di rete di un'azienda è anche logica di una soluzione / problema. Vota come vuoi.
Torxato il

@JeremyVisser al contrario, mostra che fanno diverse operazioni sul contenuto.
Baldrickk,

20

Le risposte qui sono ottime, al punto e coprono completamente questa domanda. Lascerò un ulteriore passo in giù dal codice byte per chi è interessato. Sto usando il repository più recente di CPython; le versioni precedenti si comportano in modo simile al riguardo, ma potrebbero essere in atto lievi modifiche.

Ecco una ripartizione dell'esecuzione per ciascuno di questi, BUILD_LISTa favore []e CALL_FUNCTIONa favore list().


L' BUILD_LISTistruzione:

Dovresti solo vedere l'orrore:

PyObject *list =  PyList_New(oparg);
if (list == NULL)
    goto error;
while (--oparg >= 0) {
    PyObject *item = POP();
    PyList_SET_ITEM(list, oparg, item);
}
PUSH(list);
DISPATCH();

Terribilmente contorto, lo so. Ecco come è semplice:

  • Crea un nuovo elenco con PyList_New(questo alloca principalmente la memoria per un nuovo oggetto elenco), opargsegnalando il numero di argomenti sullo stack. Dritto al punto.
  • Verifica che non sia andato storto nulla if (list==NULL).
  • Aggiungi qualsiasi argomento (nel nostro caso questo non viene eseguito) situato nello stack con PyList_SET_ITEM(una macro).

Non c'è da stupirsi che sia veloce! È fatto su misura per creare nuovi elenchi, nient'altro :-)

L' CALL_FUNCTIONistruzione:

Ecco la prima cosa che vedi quando dai una sbirciatina alla gestione del codice CALL_FUNCTION:

PyObject **sp, *res;
sp = stack_pointer;
res = call_function(&sp, oparg, NULL);
stack_pointer = sp;
PUSH(res);
if (res == NULL) {
    goto error;
}
DISPATCH();

Sembra abbastanza innocuo, vero? Beh, no, sfortunatamente no,call_function è un ragazzo semplice che chiamerà immediatamente la funzione, non può. Invece, prende l'oggetto dallo stack, prende tutti gli argomenti dello stack e quindi passa in base al tipo di oggetto; e 'un:

Stiamo chiamando il listtipo, l'argomento passato a call_functionè PyList_Type. CPython ora deve chiamare una funzione generica per gestire qualsiasi oggetto richiamabile chiamato_PyObject_FastCallKeywords , yay più chiamate di funzione.

Questa funzione effettua di nuovo alcuni controlli per alcuni tipi di funzione (che non riesco a capire perché) e quindi, dopo aver creato un dict per kwargs, se necessario , continua a chiamare_PyObject_FastCallDict .

_PyObject_FastCallDictfinalmente ci porta da qualche parte! Dopo aver eseguito ancora più controlli , prende lo tp_callslot datype quello in typecui siamo passati, cioè prende type.tp_call. Quindi procede a creare una tupla degli argomenti passati _PyStack_AsTuplee, infine, è possibile effettuare finalmente una chiamata !

tp_call, che corrisponde alle type.__call__sostituzioni e infine crea l'oggetto elenco. Chiama le liste __new__che corrispondono PyType_GenericNewe alloca memoria per essa con PyType_GenericAlloc: Questa è in realtà la parte in cui raggiunge PyList_New, finalmente . Tutti i precedenti sono necessari per gestire gli oggetti in modo generico.

Alla fine, type_call chiama list.__init__e inizializza l'elenco con tutti gli argomenti disponibili, quindi torniamo indietro nel modo in cui siamo arrivati. :-)

Infine, ricorda il LOAD_NAME, è un altro ragazzo che contribuisce qui.


È facile vedere che, quando si tratta del nostro input, Python generalmente deve saltare attraverso i cerchi per scoprire effettivamente la Cfunzione appropriata per fare il lavoro. Non ha la cortesia di chiamarlo immediatamente perché è dinamico, qualcuno potrebbe mascherarsi list( e il ragazzo fa molte persone ) e deve essere intrapreso un altro percorso.

Qui è dove list()perde molto: l'esplorazione di Python deve fare per scoprire che diamine dovrebbe fare.

La sintassi letterale, d'altra parte, significa esattamente una cosa; non può essere modificato e si comporta sempre in modo predeterminato.

Nota a piè di pagina: tutti i nomi delle funzioni sono soggetti a modifica da una versione all'altra. Il punto è ancora valido e molto probabilmente lo sarà in tutte le versioni future, è la ricerca dinamica che rallenta le cose.


13

Perché è []più veloce di list()?

Il motivo principale è che Python tratta list()esattamente come una funzione definita dall'utente, il che significa che puoi intercettarlo aliasando qualcos'altrolist e fare qualcosa di diverso (come usare il tuo elenco di sottoclassi o forse un deque).

Crea immediatamente una nuova istanza di un elenco predefinito con [].

La mia spiegazione cerca di darti l'intuizione per questo.

Spiegazione

[] è comunemente noto come sintassi letterale.

In grammatica, questo è indicato come un "elenco di visualizzazione". Dai documenti :

Una visualizzazione elenco è una serie possibilmente vuota di espressioni racchiuse tra parentesi quadre:

list_display ::=  "[" [starred_list | comprehension] "]"

Una visualizzazione elenco produce un nuovo oggetto elenco, il cui contenuto viene specificato da un elenco di espressioni o da una comprensione. Quando viene fornito un elenco di espressioni separato da virgole, i suoi elementi vengono valutati da sinistra a destra e posizionati nell'oggetto elenco in quell'ordine. Quando viene fornita una comprensione, l'elenco viene costruito dagli elementi risultanti dalla comprensione.

In breve, ciò significa che listviene creato un oggetto incorporato di tipo .

Non si può eludere questo, il che significa che Python può farlo il più rapidamente possibile.

D'altra parte, list()può essere intercettato dalla creazione di un builtin listusando il costruttore dell'elenco incorporato.

Ad esempio, supponiamo di voler creare rumorosamente i nostri elenchi:

class List(list):
    def __init__(self, iterable=None):
        if iterable is None:
            super().__init__()
        else:
            super().__init__(iterable)
        print('List initialized.')

Potremmo quindi intercettare il nome listnell'ambito globale a livello di modulo e quindi quando creiamo un list, creiamo effettivamente il nostro elenco sottotipato:

>>> list = List
>>> a_list = list()
List initialized.
>>> type(a_list)
<class '__main__.List'>

Allo stesso modo potremmo rimuoverlo dallo spazio dei nomi globale

del list

e inseriscilo nello spazio dei nomi incorporato:

import builtins
builtins.list = List

E adesso:

>>> list_0 = list()
List initialized.
>>> type(list_0)
<class '__main__.List'>

E nota che la visualizzazione dell'elenco crea un elenco incondizionatamente:

>>> list_1 = []
>>> type(list_1)
<class 'list'>

Probabilmente lo facciamo solo temporaneamente, quindi annulliamo le nostre modifiche - prima rimuovi il nuovo Listoggetto dai builtin:

>>> del builtins.list
>>> builtins.list
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: module 'builtins' has no attribute 'list'
>>> list()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'list' is not defined

Oh, no, abbiamo perso la traccia dell'originale.

Non preoccuparti, possiamo ancora ottenere list- è il tipo di un elenco letterale:

>>> builtins.list = type([])
>>> list()
[]

Così...

Perché è []più veloce di list()?

Come abbiamo visto - possiamo sovrascrivere list- ma non possiamo intercettare la creazione del tipo letterale. Quando usiamo listdobbiamo fare le ricerche per vedere se c'è qualcosa.

Quindi dobbiamo chiamare qualunque cosa abbiamo chiamato. Dalla grammatica:

Una chiamata chiama un oggetto richiamabile (ad es. Una funzione) con una serie di argomenti possibilmente vuota:

call                 ::=  primary "(" [argument_list [","] | comprehension] ")"

Possiamo vedere che fa la stessa cosa per qualsiasi nome, non solo per l'elenco:

>>> import dis
>>> dis.dis('list()')
  1           0 LOAD_NAME                0 (list)
              2 CALL_FUNCTION            0
              4 RETURN_VALUE
>>> dis.dis('doesnotexist()')
  1           0 LOAD_NAME                0 (doesnotexist)
              2 CALL_FUNCTION            0
              4 RETURN_VALUE

Non []esiste alcuna chiamata di funzione a livello di bytecode Python:

>>> dis.dis('[]')
  1           0 BUILD_LIST               0
              2 RETURN_VALUE

Va semplicemente alla costruzione dell'elenco senza ricerche o chiamate a livello di bytecode.

Conclusione

Abbiamo dimostrato che listpuò essere intercettato con il codice utente utilizzando le regole di scoping e che list()cerca un callable e quindi lo chiama.

Considerando che []è un elenco elenco, o letterale, e quindi evita la ricerca del nome e la chiamata di funzione.


2
+1 per sottolineare che puoi dirottare liste il compilatore python non può essere sicuro se restituirà davvero un elenco vuoto.
Beefster,
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.