Perché due elenchi identici hanno un footprint di memoria diverso?


155

Ho creato due elenchi l1e l2, ciascuno con un metodo di creazione diverso:

import sys

l1 = [None] * 10
l2 = [None for _ in range(10)]

print('Size of l1 =', sys.getsizeof(l1))
print('Size of l2 =', sys.getsizeof(l2))

Ma l'output mi ha sorpreso:

Size of l1 = 144
Size of l2 = 192

L'elenco creato con una comprensione dell'elenco ha una dimensione maggiore in memoria, ma altrimenti i due elenchi sono identici in Python.

Perché? È una cosa interna di CPython o qualche altra spiegazione?


2
Probabilmente, l'operatore di ripetizione invocherà alcune funzioni che dimensionano esattamente l'array sottostante. Si noti che 144 == sys.getsizeof([]) + 8*10)dove 8 è la dimensione di un puntatore.
juanpa.arrivillaga,

1
Si noti che se si cambia 10in 11, l' [None] * 11elenco ha dimensioni 152, ma la comprensione dell'elenco ha ancora dimensioni 192. La domanda precedentemente collegata non è un duplicato esatto, ma è rilevante per capire perché ciò accada.
Patrick Haugh,

Risposte:


162

Quando scrivi [None] * 10, Python sa che avrà bisogno di un elenco di esattamente 10 oggetti, quindi alloca esattamente quello.

Quando si utilizza la comprensione di un elenco, Python non sa quanto sarà necessario. Quindi aumenta gradualmente l'elenco man mano che vengono aggiunti elementi. Per ogni riallocazione alloca più spazio di quanto è immediatamente necessario, in modo da non dover riallocare per ogni elemento. È probabile che l'elenco risultante sia leggermente più grande del necessario.

È possibile visualizzare questo comportamento quando si confrontano elenchi creati con dimensioni simili:

>>> sys.getsizeof([None]*15)
184
>>> sys.getsizeof([None]*16)
192
>>> sys.getsizeof([None for _ in range(15)])
192
>>> sys.getsizeof([None for _ in range(16)])
192
>>> sys.getsizeof([None for _ in range(17)])
264

Puoi vedere che il primo metodo alloca proprio ciò che è necessario, mentre il secondo cresce periodicamente. In questo esempio, alloca abbastanza per 16 elementi e ha dovuto riallocare al raggiungimento del 17 °.


1
Sì, ha senso. Probabilmente è meglio creare liste con *quando conosco le dimensioni davanti.
Andrej Kesely, il

27
@AndrejKesely Utilizzare solo [x] * ncon immutabile xnell'elenco. L'elenco risultante conterrà riferimenti all'oggetto identico.
schwobaseggl,

5
@schwobaseggl bene, potrebbe essere quello che vuoi, ma è bene capirlo.
juanpa.arrivillaga,

19
@ juanpa.arrivillaga Vero, potrebbe essere. Ma di solito non lo è e in particolare SO è pieno di poster che si chiedono perché tutti i loro dati siano cambiati contemporaneamente: D
schwobaseggl

50

Come notato in questa domanda, la comprensione della lista usa list.appendsotto il cofano, quindi chiamerà il metodo di ridimensionamento della lista, che si sovrallocata.

Per dimostrarlo a te stesso, puoi effettivamente utilizzare il disdissasembler:

>>> code = compile('[x for x in iterable]', '', 'eval')
>>> import dis
>>> dis.dis(code)
  1           0 LOAD_CONST               0 (<code object <listcomp> at 0x10560b810, file "", line 1>)
              2 LOAD_CONST               1 ('<listcomp>')
              4 MAKE_FUNCTION            0
              6 LOAD_NAME                0 (iterable)
              8 GET_ITER
             10 CALL_FUNCTION            1
             12 RETURN_VALUE

Disassembly of <code object <listcomp> at 0x10560b810, file "", line 1>:
  1           0 BUILD_LIST               0
              2 LOAD_FAST                0 (.0)
        >>    4 FOR_ITER                 8 (to 14)
              6 STORE_FAST               1 (x)
              8 LOAD_FAST                1 (x)
             10 LIST_APPEND              2
             12 JUMP_ABSOLUTE            4
        >>   14 RETURN_VALUE
>>>

Notare il LIST_APPENDcodice operativo nello smontaggio <listcomp>dell'oggetto codice. Dai documenti :

LIST_APPEND (i)

Chiamate list.append(TOS[-i], TOS). Utilizzato per implementare la comprensione dell'elenco.

Ora, per l'operazione di ripetizione dell'elenco, abbiamo un suggerimento su cosa sta succedendo se consideriamo:

>>> import sys
>>> sys.getsizeof([])
64
>>> 8*10
80
>>> 64 + 80
144
>>> sys.getsizeof([None]*10)
144

Quindi, sembra essere in grado di allocare esattamente la dimensione. Guardando il codice sorgente , vediamo che è esattamente quello che succede:

static PyObject *
list_repeat(PyListObject *a, Py_ssize_t n)
{
    Py_ssize_t i, j;
    Py_ssize_t size;
    PyListObject *np;
    PyObject **p, **items;
    PyObject *elem;
    if (n < 0)
        n = 0;
    if (n > 0 && Py_SIZE(a) > PY_SSIZE_T_MAX / n)
        return PyErr_NoMemory();
    size = Py_SIZE(a) * n;
    if (size == 0)
        return PyList_New(0);
    np = (PyListObject *) PyList_New(size);

Vale a dire, qui: size = Py_SIZE(a) * n;. Il resto delle funzioni riempie semplicemente l'array.


"Come notato in questa domanda la comprensione della lista usa list.append sotto il cofano" Penso che sia più preciso dire che usa .extend().
Accumulazione,

@Accumulazione perché ci credi?
juanpa.arrivillaga,

Perché non aggiunge elementi uno alla volta. Quando aggiungi elementi a un elenco, stai davvero creando un nuovo elenco, con una nuova allocazione di memoria e inserendo l'elenco in quella nuova allocazione di memoria. Le comprensioni dell'elenco, d'altra parte, mettono in memoria la maggior parte dei nuovi elementi che sono già stati allocati e quando esauriscono la memoria allocata, allocano un altro pezzo di memoria, non solo per il nuovo elemento.
Accumulazione,

7
@Acccumulation Questo non è corretto. list.appendè un'operazione a tempo costante ammortizzata perché quando un elenco viene ridimensionato, viene sovrallocato. Pertanto, non tutte le operazioni di accodamento risultano in un array appena allocato. In ogni caso la domanda che ho collegato a spettacoli voi nel codice sorgente che in realtà, list comprehension fanno uso list.append,. Tornerò al mio laptop tra un momento e posso mostrarti il ​​bytecode smontato per una comprensione dell'elenco e il corrispondente LIST_APPENDcodice
operativo

3

Nessuno è un blocco di memoria, ma non è una dimensione predefinita. Inoltre, vi è una spaziatura aggiuntiva in una matrice tra gli elementi della matrice. Puoi vederlo tu stesso eseguendo:

for ele in l2:
    print(sys.getsizeof(ele))

>>>>16
16
16
16
16
16
16
16
16
16

Che non totalizza la dimensione di l2, ma piuttosto è inferiore.

print(sys.getsizeof([None]))
72

E questo è molto più grande di un decimo della dimensione di l1.

I numeri dovrebbero variare a seconda sia dei dettagli del sistema operativo sia dei dettagli dell'utilizzo corrente della memoria nel sistema operativo. La dimensione di [Nessuno] non può mai essere maggiore della memoria adiacente disponibile in cui è impostata la memorizzazione per la variabile e potrebbe essere necessario spostare la variabile se successivamente viene allocata dinamicamente per essere più grande.


1
Nonenon è effettivamente archiviato nella matrice sottostante, l'unica cosa che è memorizzata è un PyObjectpuntatore (8 byte). Tutti gli oggetti Python sono allocati sull'heap. Noneè un singleton, quindi avere un elenco con molti non è semplicemente creerà una matrice di puntatori PyObject sullo stesso Noneoggetto sull'heap (e non utilizzerà memoria aggiuntiva nel processo per ogni ulteriore None). Non sono sicuro di cosa significhi "Nessuno non ha una dimensione predefinita", ma non sembra corretto. Infine, il tuo ciclo con getsizeofogni elemento non sta dimostrando ciò che sembra pensare stia dimostrando.
juanpa.arrivillaga,

Se come dici tu è vero, la dimensione di [Nessuno] * 10 dovrebbe essere uguale alla dimensione di [Nessuno]. Ma chiaramente non è così: è stato aggiunto un po 'di spazio in più. In effetti, la dimensione di [Nessuno] ripetuta dieci volte (160) è anche inferiore alla dimensione di [Nessuno] moltiplicata per dieci. Come sottolineato, chiaramente la dimensione del puntatore su [Nessuno] è inferiore alla dimensione di [Nessuno] stesso (16 byte anziché 72 byte). Tuttavia, 160 + 32 è 192. Non credo nemmeno che la risposta precedente risolva completamente il problema. È chiaro che è allocata una piccola quantità extra di memoria (forse dipendente dallo stato della macchina).
StevenJD

"Se come dici tu è vero, la dimensione di [Nessuno] * 10 dovrebbe essere uguale alla dimensione di [Nessuno]" che cosa sto dicendo che potrebbe implicare che? Ancora una volta, sembra che ci si stia concentrando sul fatto che il buffer sottostante sia sovrallocato o che la dimensione dell'elenco includa più della dimensione del buffer sottostante (ovviamente lo fa), ma non è questo il punto di questa domanda. Ancora una volta, l'uso di gestsizeofciascuno eledi essi l2è fuorviante perché getsizeof(l2) non tiene conto della dimensione degli elementi all'interno del contenitore .
juanpa.arrivillaga,

Per provare a te stesso quell'ultima affermazione, fallo l1 = [None]; l2 = [None]*100; l3 = [l2]allora print(sys.getsizeof(l1), sys.getsizeof(l2), sys.getsizeof(l3)). si otterrà un risultato come: 72 864 72. Cioè, rispettivamente 64 + 1*8, 64 + 100*8e 64 + 1*8, di nuovo, ipotizzando un sistema a 64 bit con 8 byte dimensione del puntatore.
juanpa.arrivillaga,

1
Come ho già detto, sys.getsizeof* non tiene conto della dimensione degli articoli nel contenitore. Dai documenti : "Viene presa in considerazione solo il consumo di memoria direttamente attribuito all'oggetto, non il consumo di memoria degli oggetti a cui si riferisce ... Vedi la dimensione ricorsiva della ricetta per un esempio dell'uso ricorsivo di getsizeof () per trovare la dimensione dei contenitori e tutti i loro contenuti ".
juanpa.arrivillaga,
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.