Cosa causa la sovrallocazione di [* a]?


136

Apparentemente list(a)non sovrallocare, [x for x in a]sovrallocare in alcuni punti e [*a]sovrallocare continuamente ?

Dimensioni fino a n = 100

Ecco le dimensioni n da 0 a 12 e le dimensioni risultanti in byte per i tre metodi:

0 56 56 56
1 64 88 88
2 72 88 96
3 80 88 104
4 88 88 112
5 96 120 120
6 104 120 128
7 112 120 136
8 120 120 152
9 128 184 184
10 136 184 192
11 144 184 200
12 152 184 208

Calcolato in questo modo, riproducibile su repl.it , usando Python 3. 8 :

from sys import getsizeof

for n in range(13):
    a = [None] * n
    print(n, getsizeof(list(a)),
             getsizeof([x for x in a]),
             getsizeof([*a]))

Quindi: come funziona? Come si [*a]sovrallocano? In realtà, quale meccanismo utilizza per creare l'elenco dei risultati dall'input specificato? Usa un iteratore sopra ae usa qualcosa di simile list.append? Dov'è il codice sorgente?

( Colab con dati e codice che ha prodotto le immagini.)

Zoom su n più piccolo:

Dimensioni fino a n = 40

Zoom indietro su n più grande:

Dimensioni fino a n = 1000


1
In seguito, estendendo i casi di test sembrerebbe che la comprensione dell'elenco si comporti come la scrittura di un ciclo e l'aggiunta di ciascun elemento all'elenco, mentre [*a]sembra comportarsi come l'utilizzo di extendun elenco vuoto.
Jdehesa,

4
Potrebbe essere utile esaminare il codice byte generato per ciascuno. list(a)opera interamente in C; può allocare il nodo buffer interno per nodo mentre scorre a. [x for x in a]usa solo LIST_APPENDmolto, quindi segue il normale schema "sovrallocare un po ', riallocare quando necessario" di un elenco normale. [*a]usa BUILD_LIST_UNPACK, che ... non so cosa faccia, tranne apparentemente sovra-allocare tutto il tempo :)
Chepner

2
Inoltre, in Python 3.7, sembra che list(a)e [*a]sono identici, e sia overallocate confrontato [x for x in a], quindi ... sys.getsizeofnon potrebbe essere lo strumento giusto da utilizzare qui.
Chepner,

7
@chepner Penso che sys.getsizeofsia lo strumento giusto, mostra solo quello list(a)usato per sovrallocare. In realtà Novità di Python 3.8 lo menziona: "Il costruttore dell'elenco non sovrallocare [...]" .
Stefan Pochmann,

5
@chepner: era un bug corretto in 3.8 ; il costruttore non dovrebbe sovrassegnare.
ShadowRanger

Risposte:


81

[*a] sta facendo internamente l'equivalente C di :

  1. Crea un nuovo, vuoto list
  2. Chiamata newlist.extend(a)
  3. Ritorni list.

Quindi se estendi il test a:

from sys import getsizeof

for n in range(13):
    a = [None] * n
    l = []
    l.extend(a)
    print(n, getsizeof(list(a)),
             getsizeof([x for x in a]),
             getsizeof([*a]),
             getsizeof(l))

Provalo online!

vedrai i risultati per getsizeof([*a])e l = []; l.extend(a); getsizeof(l)sono gli stessi.

Questa è di solito la cosa giusta da fare; quando extendin genere ti aspetti di aggiungerne altri in un secondo momento, e in modo simile per il disimballaggio generalizzato, si presume che verranno aggiunte più cose una dopo l'altra. [*a]non è il caso normale; Python presuppone che ci siano più elementi o iterabili aggiunti a list( [*a, b, c, *d]), quindi la sovrallocazione salva il lavoro nel caso comune.

Al contrario, un listcostruito da un unico, presumibile iterabile (con list()) potrebbe non crescere o ridursi durante l'uso e la sovrallocazione è prematura fino a prova contraria; Python ha recentemente corretto un bug che rendeva il costruttore sovrallocato anche per input con dimensioni note .

Per quanto riguarda le listcomprensioni, sono effettivamente equivalenti a appends ripetute , quindi stai vedendo il risultato finale del normale modello di crescita di sovrallocazione quando aggiungi un elemento alla volta.

Per essere chiari, nulla di tutto ciò è una garanzia linguistica. È proprio come CPython lo implementa. Le specifiche del linguaggio Python sono generalmente indifferenti a specifici modelli di crescita in list(oltre a garantire la O(1) appends e la pops ammortizzate dalla fine). Come notato nei commenti, l'implementazione specifica cambia di nuovo in 3.9; mentre non influirà [*a], potrebbe interessare altri casi in cui quello che era "costruire un temporaneo tupledi singoli elementi e poi extendcon tuple" diventa ora più applicazioni di LIST_APPEND, che possono cambiare quando si verifica la sovrallocazione e quali numeri vanno nel calcolo.


4
@StefanPochmann: avevo già letto il codice (motivo per cui lo sapevo già). Questo è il gestore del codice byte perBUILD_LIST_UNPACK , utilizza _PyList_Extendcome l'equivalente C della chiamata extend(solo direttamente, piuttosto che dalla ricerca del metodo). Lo hanno combinato con i percorsi per la costruzione di un tuplecon disimballaggio; tuples non si sovrallocano bene per la costruzione frammentaria, quindi scompattano sempre in un list(per beneficiare della sovrallocazione) e si convertono alla tuplefine quando è quello che è stato richiesto.
ShadowRanger

4
Si noti che questo apparentemente cambia in 3.9 , dove la costruzione viene eseguita con bytecode separati ( BUILD_LIST, LIST_EXTENDper ogni cosa da decomprimere, LIST_APPENDper singoli elementi), invece di caricare tutto nello stack prima di costruire il tutto listcon un'unica istruzione in codice byte (consente compilatore per eseguire ottimizzazioni che l'istruzione all-in-one non ha permesso, come attuazione [*a, b, *c]come LIST_EXTEND, LIST_APPEND, LIST_EXTENDw / o la necessità di avvolgere bin un uno tupleper soddisfare i requisiti di BUILD_LIST_UNPACK).
ShadowRanger

18

Quadro completo di ciò che accade, basandosi sulle altre risposte e commenti (in particolare la risposta di ShadowRanger , che spiega anche perché è stato fatto così).

Disassemblaggio degli spettacoli che BUILD_LIST_UNPACKvengono utilizzati:

>>> import dis
>>> dis.dis('[*a]')
  1           0 LOAD_NAME                0 (a)
              2 BUILD_LIST_UNPACK        1
              4 RETURN_VALUE

Questo è gestito inceval.c , che costruisce un elenco vuoto e la estende (con a):

        case TARGET(BUILD_LIST_UNPACK): {
            ...
            PyObject *sum = PyList_New(0);
              ...
                none_val = _PyList_Extend((PyListObject *)sum, PEEK(i));

_PyList_Extend utilizza list_extend :

_PyList_Extend(PyListObject *self, PyObject *iterable)
{
    return list_extend(self, iterable);
}

Che chiama list_resizecon la somma delle dimensioni :

list_extend(PyListObject *self, PyObject *iterable)
    ...
        n = PySequence_Fast_GET_SIZE(iterable);
        ...
        m = Py_SIZE(self);
        ...
        if (list_resize(self, m + n) < 0) {

E questo si sovrascrive come segue:

list_resize(PyListObject *self, Py_ssize_t newsize)
{
  ...
    new_allocated = (size_t)newsize + (newsize >> 3) + (newsize < 9 ? 3 : 6);

Controlliamo quello. Calcola il numero atteso di punti con la formula sopra, e calcola la dimensione in byte prevista moltiplicandola per 8 (dato che sto usando Python a 64 bit qui) e aggiungendo una dimensione in byte di un elenco vuoto (ovvero, un overhead costante di un oggetto elenco) :

from sys import getsizeof
for n in range(13):
    a = [None] * n
    expected_spots = n + (n >> 3) + (3 if n < 9 else 6)
    expected_bytesize = getsizeof([]) + expected_spots * 8
    real_bytesize = getsizeof([*a])
    print(n,
          expected_bytesize,
          real_bytesize,
          real_bytesize == expected_bytesize)

Produzione:

0 80 56 False
1 88 88 True
2 96 96 True
3 104 104 True
4 112 112 True
5 120 120 True
6 128 128 True
7 136 136 True
8 152 152 True
9 184 184 True
10 192 192 True
11 200 200 True
12 208 208 True

Partite ad eccezione di n = 0, che in list_extendrealtà scorciatoie , quindi in realtà anche quelle corrispondenti:

        if (n == 0) {
            ...
            Py_RETURN_NONE;
        }
        ...
        if (list_resize(self, m + n) < 0) {

8

Questi saranno i dettagli di implementazione dell'interprete CPython e quindi potrebbero non essere coerenti con altri interpreti.

Detto questo, puoi vedere dove list(a)arrivano la comprensione e i comportamenti:

https://github.com/python/cpython/blob/master/Objects/listobject.c#L36

In particolare per la comprensione:

 * The growth pattern is:  0, 4, 8, 16, 25, 35, 46, 58, 72, 88, ...
...

new_allocated = (size_t)newsize + (newsize >> 3) + (newsize < 9 ? 3 : 6);

Appena sotto quelle linee, c'è list_preallocate_exactquale viene usato quando si chiama list(a).


1
[*a]non aggiunge singoli elementi uno alla volta. Ha il suo bytecode dedicato, che consente l'inserimento di massa tramite extend.
ShadowRanger

Gotcha - Immagino di non aver approfondito abbastanza. Rimossa la sezione su[*a]
Randy
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.