A tuple
occupa meno spazio di memoria in Python:
>>> a = (1,2,3)
>>> a.__sizeof__()
48
mentre list
s occupa più spazio di memoria:
>>> b = [1,2,3]
>>> b.__sizeof__()
64
Cosa succede internamente alla gestione della memoria Python?
A tuple
occupa meno spazio di memoria in Python:
>>> a = (1,2,3)
>>> a.__sizeof__()
48
mentre list
s occupa più spazio di memoria:
>>> b = [1,2,3]
>>> b.__sizeof__()
64
Cosa succede internamente alla gestione della memoria Python?
Risposte:
Presumo che tu stia usando CPython e con 64 bit (ho ottenuto gli stessi risultati sul mio CPython 2.7 a 64 bit). Potrebbero esserci differenze in altre implementazioni Python o se hai un Python a 32 bit.
Indipendentemente dall'implementazione, le list
s sono di dimensione variabile mentre le tuple
s sono di dimensione fissa.
Quindi tuple
s può memorizzare gli elementi direttamente all'interno della struttura, le liste d'altra parte necessitano di uno strato di riferimento indiretto (memorizza un puntatore agli elementi). Questo livello di riferimento indiretto è un puntatore, su sistemi a 64 bit che è 64 bit, quindi 8 byte.
Ma c'è un'altra cosa che list
fa: allocano eccessivamente. Altrimenti list.append
sarebbe sempreO(n)
un'operazione - per renderlo ammortizzato (molto più velocemente !!!) sovra-alloca. Ma ora deve tenere traccia delle dimensioni assegnate e delle dimensioni riempite ( è necessario memorizzare solo una dimensione, perché le dimensioni allocate e riempite sono sempre identiche). Ciò significa che ogni elenco deve memorizzare un'altra "dimensione" che su sistemi a 64 bit è un intero a 64 bit, ancora 8 byte.O(1)
tuple
Quindi list
s ha bisogno di almeno 16 byte di memoria in più rispetto a tuple
s. Perché ho detto "almeno"? A causa della sovra-allocazione. Sovrallocazione significa che alloca più spazio del necessario. Tuttavia, la quantità di sovrallocazione dipende da "come" crei l'elenco e dalla cronologia di aggiunta / eliminazione:
>>> l = [1,2,3]
>>> l.__sizeof__()
64
>>> l.append(4) # triggers re-allocation (with over-allocation), because the original list is full
>>> l.__sizeof__()
96
>>> l = []
>>> l.__sizeof__()
40
>>> l.append(1) # re-allocation with over-allocation
>>> l.__sizeof__()
72
>>> l.append(2) # no re-alloc
>>> l.append(3) # no re-alloc
>>> l.__sizeof__()
72
>>> l.append(4) # still has room, so no over-allocation needed (yet)
>>> l.__sizeof__()
72
Ho deciso di creare alcune immagini per accompagnare la spiegazione sopra. Forse sono utili
Questo è il modo in cui (schematicamente) viene archiviato in memoria nel tuo esempio. Ho evidenziato le differenze con i cicli rossi (a mano libera):
Questa è in realtà solo un'approssimazione perché gli int
oggetti sono anche oggetti Python e CPython riutilizza persino interi piccoli, quindi una rappresentazione probabilmente più accurata (sebbene non leggibile) degli oggetti in memoria sarebbe:
Link utili:
tuple
struct nel repository CPython per Python 2.7list
struct nel repository CPython per Python 2.7int
struct nel repository CPython per Python 2.7Nota che in __sizeof__
realtà non restituisce la dimensione "corretta"! Restituisce solo la dimensione dei valori memorizzati. Tuttavia quando usi sys.getsizeof
il risultato è diverso:
>>> import sys
>>> l = [1,2,3]
>>> t = (1, 2, 3)
>>> sys.getsizeof(l)
88
>>> sys.getsizeof(t)
72
Sono disponibili 24 byte "extra". Questi sono reali , questo è l'overhead del garbage collector che non è considerato nel __sizeof__
metodo. Questo perché generalmente non dovresti usare direttamente i metodi magici: usa le funzioni che sanno come gestirli, in questo caso: sys.getsizeof
(che in realtà aggiunge l'overhead GC al valore restituito da __sizeof__
).
list
allocazione di memoria stackoverflow.com/questions/40018398/...
list()
o una comprensione di elenchi.
Farò un'immersione più approfondita nella base di codice CPython in modo da poter vedere come vengono effettivamente calcolate le dimensioni. Nel tuo esempio specifico , non sono state eseguite allocazioni eccessive, quindi non lo toccherò .
Userò i valori a 64 bit qui, come te.
La dimensione per list
s viene calcolata dalla seguente funzione list_sizeof
:
static PyObject *
list_sizeof(PyListObject *self)
{
Py_ssize_t res;
res = _PyObject_SIZE(Py_TYPE(self)) + self->allocated * sizeof(void*);
return PyInt_FromSsize_t(res);
}
Ecco Py_TYPE(self)
una macro che afferra il ob_type
di self
(ritorno PyList_Type
) mentre _PyObject_SIZE
è un'altra macro che afferra tp_basicsize
da quel tipo. tp_basicsize
viene calcolato come sizeof(PyListObject)
dove si PyListObject
trova la struttura dell'istanza.
La PyListObject
struttura ha tre campi:
PyObject_VAR_HEAD # 24 bytes
PyObject **ob_item; # 8 bytes
Py_ssize_t allocated; # 8 bytes
questi hanno commenti (che ho tagliato) che spiegano cosa sono, segui il link sopra per leggerli. PyObject_VAR_HEAD
si espande in tre campi da 8 byte ( ob_refcount
, ob_type
e ob_size
) quindi un 24
contributo in byte.
Quindi per ora res
è:
sizeof(PyListObject) + self->allocated * sizeof(void*)
o:
40 + self->allocated * sizeof(void*)
Se l'istanza dell'elenco contiene elementi allocati. la seconda parte calcola il loro contributo. self->allocated
, come suggerisce il nome, contiene il numero di elementi allocati.
Senza alcun elemento, la dimensione degli elenchi è calcolata come:
>>> [].__sizeof__()
40
cioè la dimensione della struttura dell'istanza.
tuple
gli oggetti non definiscono una tuple_sizeof
funzione. Invece, usano object_sizeof
per calcolare la loro dimensione:
static PyObject *
object_sizeof(PyObject *self, PyObject *args)
{
Py_ssize_t res, isize;
res = 0;
isize = self->ob_type->tp_itemsize;
if (isize > 0)
res = Py_SIZE(self) * isize;
res += self->ob_type->tp_basicsize;
return PyInt_FromSsize_t(res);
}
Questo, come per list
s, afferra il tp_basicsize
e, se l'oggetto ha un diverso da zero tp_itemsize
(cioè ha istanze di lunghezza variabile), moltiplica il numero di elementi nella tupla (che ottiene tramite Py_SIZE
) con tp_itemsize
.
tp_basicsize
usa ancora sizeof(PyTupleObject)
dove la PyTupleObject
struttura contiene :
PyObject_VAR_HEAD # 24 bytes
PyObject *ob_item[1]; # 8 bytes
Quindi, senza alcun elemento (ovvero, Py_SIZE
restituisce 0
) la dimensione delle tuple vuote è uguale a sizeof(PyTupleObject)
:
>>> ().__sizeof__()
24
eh? Bene, ecco una stranezza per la quale non ho trovato una spiegazione, il tp_basicsize
of tuple
s è in realtà calcolato come segue:
sizeof(PyTupleObject) - sizeof(PyObject *)
perché un 8
byte aggiuntivo viene rimosso tp_basicsize
è qualcosa che non sono stato in grado di scoprire. (Vedi il commento di MSeifert per una possibile spiegazione)
Ma questa è fondamentalmente la differenza nel tuo esempio specifico . list
s mantiene anche un numero di elementi allocati che aiuta a determinare quando sovra-allocare di nuovo.
Ora, quando vengono aggiunti elementi aggiuntivi, le liste eseguono effettivamente questa sovrallocazione per ottenere l'aggiunta di O (1). Ciò si traduce in dimensioni maggiori poiché le copertine di MSeifert nella sua risposta.
ob_item[1]
sia principalmente un segnaposto (quindi ha senso che sia sottratto dalla dimensione di base). Il tuple
è assegnato utilizzando PyObject_NewVar
. Non ho capito i dettagli, quindi è solo un'ipotesi
La risposta di MSeifert lo copre ampiamente; per mantenerlo semplice puoi pensare a:
tuple
è immutabile. Una volta impostato, non puoi modificarlo. Quindi sai in anticipo quanta memoria devi allocare per quell'oggetto.
list
è mutevole. Puoi aggiungere o rimuovere elementi ao da esso. Deve conoscerne le dimensioni (per impl. Interno). Si ridimensiona secondo necessità.
Non ci sono pasti gratuiti : queste funzionalità hanno un costo. Da qui l'overhead in memoria per gli elenchi.
La dimensione della tupla è prefissata, il che significa che durante l'inizializzazione della tupla l'interprete alloca abbastanza spazio per i dati contenuti, e questa è la fine, dandogli che è immutabile (non può essere modificato), mentre una lista è un oggetto mutabile quindi implica dinamico allocazione della memoria, in modo da evitare di allocare spazio ogni volta che aggiungi o modifichi l'elenco (alloca spazio sufficiente per contenere i dati modificati e copia i dati su di esso), alloca spazio aggiuntivo per future aggiunte, modifiche, ... che praticamente lo riassume.