Perché la copia di un elenco mescolato è molto più lenta?


89

La copia di un range(10**6)elenco mescolato dieci volte mi richiede circa 0,18 secondi: (queste sono cinque corse)

0.175597017661
0.173731403198
0.178601711594
0.180330912952
0.180811964451

Copiare dieci volte l'elenco non mescolato richiede circa 0,05 secondi:

0.058402235973
0.0505464636856
0.0509734306934
0.0526022752744
0.0513324916184

Ecco il mio codice di prova:

from timeit import timeit
import random

a = range(10**6)
random.shuffle(a)    # Remove this for the second test.
a = list(a)          # Just an attempt to "normalize" the list.
for _ in range(5):
    print timeit(lambda: list(a), number=10)

Ho anche provato a copiare con a[:], i risultati erano simili (cioè, grande differenza di velocità)

Perché la grande differenza di velocità? Conosco e capisco la differenza di velocità nel famoso Perché è più veloce elaborare un array ordinato rispetto a un array non ordinato? esempio, ma qui la mia elaborazione non ha decisioni. Sta solo copiando ciecamente i riferimenti all'interno della lista, no?

Sto usando Python 2.7.12 su Windows 10.

Modifica: ho provato anche Python 3.5.2 ora, i risultati erano quasi gli stessi (mescolati costantemente intorno a 0,17 secondi, non mescolati costantemente intorno a 0,05 secondi). Ecco il codice per questo:

a = list(range(10**6))
random.shuffle(a)
a = list(a)
for _ in range(5):
    print(timeit(lambda: list(a), number=10))


5
Per favore non gridare contro di me, stavo cercando di aiutarti! Dopo aver modificato l'ordine, ottengo approssimativamente 0.25in ogni iterazione di ciascuno dei test. Quindi sulla mia piattaforma l'ordine è importante.
barak manos

1
@vaultah Grazie, ma l'ho letto ora e non sono d'accordo. Quando ho visto il codice lì, ho subito pensato ai colpi / mancati cache degli int, che è anche la conclusione dell'autore. Ma il suo codice aggiunge i numeri, il che richiede di guardarli. Il mio codice no. Il mio ha solo bisogno di copiare i riferimenti, non di accedervi attraverso di essi.
Stefan Pochmann

2
C'è una risposta completa in un link di @vaultah (sei leggermente in disaccordo in questo momento, vedo). Ma in ogni caso penso ancora che non dovremmo usare python per funzionalità di basso livello, e quindi preoccuparci. Ma questo argomento è comunque interessante, grazie.
Nikolay Prokopyev

1
@NikolayProkopyev Sì, non sono preoccupato, l'ho notato mentre facevo qualcos'altro, non riuscivo a spiegarlo e mi sono incuriosito. E sono contento di aver chiesto e di avere una risposta ora :-)
Stefan Pochmann

Risposte:


100

La cosa interessante è che dipende dall'ordine in cui gli interi vengono creati per la prima volta. Ad esempio, invece di shufflecreare una sequenza casuale con random.randint:

from timeit import timeit
import random

a = [random.randint(0, 10**6) for _ in range(10**6)]
for _ in range(5):
    print(timeit(lambda: list(a), number=10))

È veloce come copiare il tuo file list(range(10**6)) (primo e veloce esempio).

Tuttavia, quando mescoli, i tuoi numeri interi non sono più nell'ordine in cui sono stati creati per la prima volta, questo è ciò che lo rende lento.

Un veloce intermezzo:

  • Tutti gli oggetti Python sono sull'heap, quindi ogni oggetto è un puntatore.
  • La copia di un elenco è un'operazione superficiale.
  • Tuttavia Python utilizza il conteggio dei riferimenti, quindi quando un oggetto viene inserito in un nuovo contenitore, il conteggio dei riferimenti deve essere incrementato ( Py_INCREFinlist_slice ), quindi Python ha davvero bisogno di andare dove si trova l'oggetto. Non può semplicemente copiare il riferimento.

Quindi, quando copi la tua lista ottieni ogni elemento di quella lista e la metti "così com'è" nella nuova lista. Quando il tuo prossimo oggetto è stato creato poco dopo quello attuale, c'è una buona probabilità (nessuna garanzia!) Che venga salvato accanto ad esso nell'heap.

Supponiamo che ogni volta che il computer carica un elemento nella cache carichi anche gli elementi xsuccessivi nella memoria (località della cache). Quindi il tuo computer può eseguire l'incremento del conteggio dei riferimenti per gli x+1elementi nella stessa cache!

Con la sequenza mescolata carica ancora gli elementi successivi in ​​memoria, ma questi non sono quelli successivi nell'elenco. Quindi non può eseguire l'incremento del conteggio dei riferimenti senza cercare "realmente" l'elemento successivo.

TL; DR: La velocità effettiva dipende da ciò che è accaduto prima della copia: in quale ordine sono stati creati questi elementi e in quale ordine sono presenti nell'elenco.


Puoi verificarlo guardando id:

Dettagli sull'implementazione di CPython: questo è l'indirizzo dell'oggetto in memoria.

a = list(range(10**6, 10**6+100))
for item in a:
    print(id(item))

Solo per mostrare un breve estratto:

1496489995888
1496489995920  # +32
1496489995952  # +32
1496489995984  # +32
1496489996016  # +32
1496489996048  # +32
1496489996080  # +32
1496489996112
1496489996144
1496489996176
1496489996208
1496489996240
1496507297840
1496507297872
1496507297904
1496507297936
1496507297968
1496507298000
1496507298032
1496507298064
1496507298096
1496507298128
1496507298160
1496507298192

Quindi questi oggetti sono davvero "uno accanto all'altro sul mucchio". Con shuffleloro non sono:

import random
a = list(range(10**6, 100+10**6))
random.shuffle(a)
last = None
for item in a:
    if last is not None:
        print('diff', id(item) - id(last))
    last = item

Il che mostra che questi non sono davvero uno accanto all'altro nella memoria:

diff 736
diff -64
diff -17291008
diff -128
diff 288
diff -224
diff 17292032
diff -1312
diff 1088
diff -17292384
diff 17291072
diff 608
diff -17290848
diff 17289856
diff 928
diff -672
diff 864
diff -17290816
diff -128
diff -96
diff 17291552
diff -192
diff 96
diff -17291904
diff 17291680
diff -1152
diff 896
diff -17290528
diff 17290816
diff -992
diff 448

Nota importante:

Non ci ho pensato io stesso. La maggior parte delle informazioni si possono trovare nel post del blog di Ricky Stewart .

Questa risposta è basata sull'implementazione "ufficiale" di Python in CPython. I dettagli in altre implementazioni (Jython, PyPy, IronPython, ...) potrebbero essere diversi. Grazie @ JörgWMittag per averlo sottolineato .


6
@augurar La copia di un riferimento implica l'incremento del contatore di riferimenti che si trova nell'oggetto (quindi l'accesso all'oggetto è inevitabile)
Leon

1
@StefanPochmann La funzione che esegue la copia è list_slicee nella riga 453 puoi vedere la Py_INCREF(v);chiamata che deve accedere all'oggetto allocato nell'heap.
MSeifert

1
@MSeifert Un altro buon esperimento sta usando a = [0] * 10**7(da 10 ** 6 perché era troppo instabile), che è anche più veloce dell'uso a = range(10**7)(di un fattore di circa 1,25). Chiaramente perché è ancora meglio per la cache.
Stefan Pochmann

1
Mi stavo solo chiedendo perché ho ottenuto numeri interi a 32 bit su un computer a 64 bit con Python 64bit. Ma in realtà va bene anche per il caching :-) Anche [0,1,2,3]*((10**6) // 4)è veloce quanto a = [0] * 10**6. Tuttavia, con gli interi da 0 a 255 c'è un altro fatto in arrivo: questi sono internati quindi con questi l'ordine di creazione (all'interno del tuo script) non è più importante - perché vengono creati quando avvii Python.
MSeifert

2
Si noti che delle quattro implementazioni Python pronte per la produzione attualmente esistenti, solo una utilizza il conteggio dei riferimenti. Quindi, questa analisi si applica davvero solo a una singola implementazione.
Jörg W Mittag

24

Quando si mescolano gli elementi dell'elenco, hanno una località di riferimento peggiore, con conseguente peggioramento delle prestazioni della cache.

Potresti pensare che la copia dell'elenco copi solo i riferimenti, non gli oggetti, quindi la loro posizione nell'heap non dovrebbe avere importanza. Tuttavia, la copia implica ancora l'accesso a ogni oggetto per modificare il refcount.


Questa potrebbe essere una risposta migliore per me (almeno se avesse un collegamento alla "prova" come quella di MSeifert) dato che questo è tutto ciò che mi mancava ed è molto succinto, ma penso che continuerò con MSeifert come penso che potrebbe essere meglio per gli altri. Ho votato anche questo, però, grazie.
Stefan Pochmann

Aggiungerà anche che i pentioidi, gli atleti ecc. Hanno una logica mistica per rilevare i modelli di indirizzo e inizieranno a precaricare i dati quando vedono un modello. Che in questo caso, potrebbe intervenire per precaricare i dati (riducendo i mancati riscontri nella cache) quando i numeri sono in ordine. Questo effetto si aggiunge, ovviamente, all'aumento della% di visite in località.
Greggo

5

Come spiegato da altri, non si tratta solo di copiare i riferimenti, ma aumenta anche il conteggio dei riferimenti all'interno degli oggetti e quindi si accede agli oggetti e la cache gioca un ruolo.

Qui voglio solo aggiungere altri esperimenti. Non tanto per mescolare o non mescolare (dove l'accesso a un elemento potrebbe perdere la cache ma ottenere i seguenti elementi nella cache in modo che vengano colpiti). Ma sulla ripetizione di elementi, dove gli accessi successivi dello stesso elemento potrebbero colpire la cache perché l'elemento è ancora nella cache.

Testare un intervallo normale:

>>> from timeit import timeit
>>> a = range(10**7)
>>> [timeit(lambda: list(a), number=100) for _ in range(3)]
[5.1915339142808925, 5.1436351868889645, 5.18055115701749]

Un elenco della stessa dimensione ma con un solo elemento ripetuto più e più volte è più veloce perché colpisce la cache tutto il tempo:

>>> a = [0] * 10**7
>>> [timeit(lambda: list(a), number=100) for _ in range(3)]
[4.125743135926939, 4.128927210087596, 4.0941229388550795]

E non sembra importare quale numero sia:

>>> a = [1234567] * 10**7
>>> [timeit(lambda: list(a), number=100) for _ in range(3)]
[4.124106479141709, 4.156590225249886, 4.219242600790949]

È interessante notare che diventa ancora più veloce quando ripeto invece gli stessi due o quattro elementi:

>>> a = [0, 1] * (10**7 / 2)
>>> [timeit(lambda: list(a), number=100) for _ in range(3)]
[3.130586101607932, 3.1001001764957294, 3.1318465707127814]

>>> a = [0, 1, 2, 3] * (10**7 / 4)
>>> [timeit(lambda: list(a), number=100) for _ in range(3)]
[3.096105435911994, 3.127148431279352, 3.132872673690855]

Immagino che a qualcosa non piaccia lo stesso singolo contatore aumentato tutto il tempo. Forse un po 'di stallo del gasdotto perché ogni aumento deve aspettare il risultato dell'aumento precedente, ma questa è un'ipotesi folle.

Ad ogni modo, provando questo per un numero ancora maggiore di elementi ripetuti:

from timeit import timeit
for e in range(26):
    n = 2**e
    a = range(n) * (2**25 / n)
    times = [timeit(lambda: list(a), number=20) for _ in range(3)]
    print '%8d ' % n, '  '.join('%.3f' % t for t in times), ' => ', sum(times) / 3

L'output (la prima colonna è il numero di elementi diversi, per ciascuno provo tre volte e poi prendo la media):

       1  2.871  2.828  2.835  =>  2.84446732686
       2  2.144  2.097  2.157  =>  2.13275338734
       4  2.129  2.297  2.247  =>  2.22436720645
       8  2.151  2.174  2.170  =>  2.16477771575
      16  2.164  2.159  2.167  =>  2.16328197911
      32  2.102  2.117  2.154  =>  2.12437970598
      64  2.145  2.133  2.126  =>  2.13462250728
     128  2.135  2.122  2.137  =>  2.13145065221
     256  2.136  2.124  2.140  =>  2.13336283943
     512  2.140  2.188  2.179  =>  2.1688431668
    1024  2.162  2.158  2.167  =>  2.16208440826
    2048  2.207  2.176  2.213  =>  2.19829998424
    4096  2.180  2.196  2.202  =>  2.19291917834
    8192  2.173  2.215  2.188  =>  2.19207065277
   16384  2.258  2.232  2.249  =>  2.24609975704
   32768  2.262  2.251  2.274  =>  2.26239771771
   65536  2.298  2.264  2.246  =>  2.26917420394
  131072  2.285  2.266  2.313  =>  2.28767871168
  262144  2.351  2.333  2.366  =>  2.35030805124
  524288  2.932  2.816  2.834  =>  2.86047313113
 1048576  3.312  3.343  3.326  =>  3.32721167007
 2097152  3.461  3.451  3.547  =>  3.48622758473
 4194304  3.479  3.503  3.547  =>  3.50964316455
 8388608  3.733  3.496  3.532  =>  3.58716466865
16777216  3.583  3.522  3.569  =>  3.55790996695
33554432  3.550  3.556  3.512  =>  3.53952594744

Quindi da circa 2,8 secondi per un singolo elemento (ripetuto) scende a circa 2,2 secondi per 2, 4, 8, 16, ... elementi diversi e rimane a circa 2,2 secondi fino alle centomila. Penso che questo utilizzi la mia cache L2 (4 × 256 KB, ho un i7-6700 ).

Quindi in pochi passaggi, i tempi salgono a 3,5 secondi. Penso che questo utilizzi un mix della mia cache L2 e della mia cache L3 (8 MB) fino a quando non è "esaurita".

Alla fine rimane intorno ai 3,5 secondi, immagino perché le mie cache non aiutano più con gli elementi ripetuti.


0

Prima della riproduzione casuale, quando allocati nell'heap, gli oggetti indice adiacenti sono adiacenti in memoria e la frequenza di accesso alla memoria è alta quando si accede; dopo lo shuffle, l'oggetto dell'indice adiacente della nuova lista non è in memoria. Adiacente, il tasso di successo è molto basso.

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.