Perché a.insert (0,0) è molto più lento di uno [0: 0] = [0]?


61

L'uso della insertfunzione di un elenco è molto più lento del raggiungimento dello stesso effetto con l'assegnazione delle sezioni:

> python -m timeit -n 100000 -s "a=[]" "a.insert(0,0)"
100000 loops, best of 5: 19.2 usec per loop

> python -m timeit -n 100000 -s "a=[]" "a[0:0]=[0]"
100000 loops, best of 5: 6.78 usec per loop

(Si noti che a=[]è solo l'installazione, quindi ainizia vuota ma poi cresce fino a 100.000 elementi.)

All'inizio ho pensato che forse fosse la ricerca degli attributi o l'overhead di una funzione, ma l'inserimento vicino alla fine mostra che è trascurabile:

> python -m timeit -n 100000 -s "a=[]" "a.insert(-1,0)"
100000 loops, best of 5: 79.1 nsec per loop

Perché la funzione "inserisci singolo elemento" dedicata presumibilmente più semplice è molto più lenta?

Posso anche riprodurlo su repl.it :

from timeit import repeat

for _ in range(3):
  for stmt in 'a.insert(0,0)', 'a[0:0]=[0]', 'a.insert(-1,0)':
    t = min(repeat(stmt, 'a=[]', number=10**5))
    print('%.6f' % t, stmt)
  print()

# Example output:
#
# 4.803514 a.insert(0,0)
# 1.807832 a[0:0]=[0]
# 0.012533 a.insert(-1,0)
#
# 4.967313 a.insert(0,0)
# 1.821665 a[0:0]=[0]
# 0.012738 a.insert(-1,0)
#
# 5.694100 a.insert(0,0)
# 1.899940 a[0:0]=[0]
# 0.012664 a.insert(-1,0)

Uso Python 3.8.1 a 32 bit su Windows 10 a 64 bit.
repl.it utilizza Python 3.8.1 a 64 bit su Linux a 64 bit.


Interessante notare che a=[]; a[0:0]=[0]fa lo stesso dia=[]; a[100:200]=[0]
smac89 del

C'è qualche motivo per cui lo stai testando con solo un elenco vuoto?
MisterMiyagi,

@MisterMiyagi Bene, devo iniziare con qualcosa . Si noti che è vuoto solo prima del primo inserimento e cresce fino a 100.000 elementi durante il benchmark.
Heap Overflow

@ smac89 si a=[1,2,3];a[100:200]=[4]sta aggiungendo 4alla fine della lista ainteressante.
Ch3steR

1
@ smac89 Anche se è vero, non ha davvero a che fare con la domanda e temo che possa indurre in errore qualcuno a pensare che sto facendo un benchmark a=[]; a[0:0]=[0]o che a[0:0]=[0]fa lo stesso di a[100:200]=[0]...
Heap Overflow

Risposte:


57

Penso che sia probabilmente solo che si sono dimenticati di usare memmovein list.insert. Se dai un'occhiata al codice list.insert utilizzato per spostare gli elementi, puoi vedere che è solo un ciclo manuale:

for (i = n; --i >= where; )
    items[i+1] = items[i];

mentre list.__setitem__sul percorso di assegnazione delle sezioni utilizzamemmove :

memmove(&item[ihigh+d], &item[ihigh],
    (k - ihigh)*sizeof(PyObject *));

memmove in genere ha molta ottimizzazione, come sfruttare le istruzioni SSE / AVX.


5
Grazie. Creato un problema facendo riferimento a questo.
Heap Overflow

7
Se l'interprete è stato creato con -O3la vettorializzazione abilitata, quel ciclo manuale potrebbe essere compilato in modo efficiente. Ma a meno che il compilatore non riconosca il loop come un memmove e lo compili in una vera chiamata memmove, può solo sfruttare le estensioni del set di istruzioni abilitate al momento della compilazione. (Va bene se stai costruendo il tuo -march=native, non tanto per i binari di distro costruiti con la linea di base). E GCC non srotolerà i loop di default a meno che tu non usi PGO ( -fprofile-generate/ run / ...-use)
Peter Cordes

@PeterCordes Ti capisco correttamente che se il compilatore lo compila in una memmovechiamata reale , che può quindi sfruttare tutti gli interni presenti al momento dell'esecuzione?
Heap Overflow

1
@HeapOverflow: Sì. Su GNU / Linux, ad esempio, glibc sovraccarica la risoluzione dei simboli del linker dinamico con una funzione che seleziona la migliore versione asm di memmove scritta a mano per questa macchina in base ai risultati salvati del rilevamento della CPU. (es. su x86, usa una funzione di glibc init cpuid). Lo stesso per molte altre funzioni mem / str. Quindi le distribuzioni possono essere compilate solo -O2per creare binari run -where, ma almeno memcpy / memmove usano un ciclo AVX non caricato caricando / memorizzando 32 byte per istruzione. (O anche AVX512 sulle poche CPU in cui è una buona idea; penso solo a Xeon Phi.)
Peter Cordes

1
@HeapOverflow: No, diverse memmoveversioni sono disponibili in libc.so, la libreria condivisa. Per ciascuna funzione, l'invio avviene una volta, durante la risoluzione del simbolo (associazione anticipata o alla prima chiamata con associazione tradizionale pigra). Come ho detto, non fa altro che sovraccaricare / agganciare il modo in cui avviene il collegamento dinamico, non avvolgendo la funzione stessa. (in particolare tramite il meccanismo ifunc di GCC: code.woboq.org/userspace/glibc/sysdeps/x86_64/multiarch/… ). Correlati: per memset la solita scelta sulle moderne CPU è __memset_avx2_unaligned_erms vedere questa domanda e risposta
Peter Cordes
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.