Perché le tuple occupano meno spazio in memoria rispetto agli elenchi?


105

A tupleoccupa meno spazio di memoria in Python:

>>> a = (1,2,3)
>>> a.__sizeof__()
48

mentre lists occupa più spazio di memoria:

>>> b = [1,2,3]
>>> b.__sizeof__()
64

Cosa succede internamente alla gestione della memoria Python?


1
Non sono sicuro di come funzioni internamente, ma l'oggetto list ha almeno più funzioni come ad esempio append che la tupla non ha. Ha quindi senso che la tupla come tipo di oggetto più semplice sia più piccola
Metareven

Penso che dipenda anche da macchina a macchina .... per me quando controllo a = (1,2,3) richiede 72 eb = [1,2,3] richiede 88.
Amrit

6
Le tuple Python sono immutabili. Gli oggetti modificabili hanno un sovraccarico aggiuntivo per gestire le modifiche al runtime.
Lee Daniel Crocker

@ Metareven il numero di metodi di un tipo non influisce sullo spazio di memoria occupato dalle istanze. L'elenco dei metodi e il relativo codice vengono gestiti dal prototipo dell'oggetto, ma le istanze memorizzano solo dati e variabili interne.
jjmontes

Risposte:


144

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 lists sono di dimensione variabile mentre le tuples sono di dimensione fissa.

Quindi tuples 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 listfa: allocano eccessivamente. Altrimenti list.appendsarebbe 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 lists ha bisogno di almeno 16 byte di memoria in più rispetto a tuples. 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

immagini

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):

inserisci qui la descrizione dell'immagine

Questa è in realtà solo un'approssimazione perché gli intoggetti sono anche oggetti Python e CPython riutilizza persino interi piccoli, quindi una rappresentazione probabilmente più accurata (sebbene non leggibile) degli oggetti in memoria sarebbe:

inserisci qui la descrizione dell'immagine

Link utili:

Nota che in __sizeof__realtà non restituisce la dimensione "corretta"! Restituisce solo la dimensione dei valori memorizzati. Tuttavia quando usi sys.getsizeofil 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__).


Ri " Quindi le liste richiedono almeno 16 byte di memoria in più rispetto alle tuple. ", Non sarebbe 8? Una dimensione per le tuple e due dimensioni per gli elenchi significa una dimensione extra per gli elenchi.
ikegami

1
Sì, l'elenco ha una "dimensione" extra (8 byte) ma memorizza anche un puntatore (8 byte) all '"array di PyObject" invece di memorizzarli direttamente nella struttura (cosa fa la tupla). 8 + 8 = 16.
MSeifert

2
Un altro utile link di listallocazione di memoria stackoverflow.com/questions/40018398/...
vishes_shell

@vishes_shell Questo non è realmente correlato alla domanda perché il codice nella domanda non si alloca affatto . Ma sì, è utile nel caso in cui desideri saperne di più sulla quantità di sovra-allocazione durante l'utilizzo list()o una comprensione di elenchi.
MSeifert

1
@ user3349993 Le tuple non sono modificabili, quindi non puoi aggiungere a una tupla o rimuovere un elemento da una tupla.
MSeifert

31

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 lists 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_typedi self(ritorno PyList_Type) mentre _PyObject_SIZEè un'altra macro che afferra tp_basicsizeda quel tipo. tp_basicsizeviene calcolato come sizeof(PyListObject)dove si PyListObjecttrova la struttura dell'istanza.

La PyListObjectstruttura 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_HEADsi espande in tre campi da 8 byte ( ob_refcount, ob_typee ob_size) quindi un 24contributo 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.


tuplegli oggetti non definiscono una tuple_sizeoffunzione. Invece, usano object_sizeofper 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 lists, afferra il tp_basicsizee, 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_basicsizeusa ancora sizeof(PyTupleObject)dove la PyTupleObjectstruttura contiene :

PyObject_VAR_HEAD       # 24 bytes 
PyObject *ob_item[1];   # 8  bytes

Quindi, senza alcun elemento (ovvero, Py_SIZErestituisce 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_basicsizeof tuples è in realtà calcolato come segue:

sizeof(PyTupleObject) - sizeof(PyObject *)

perché un 8byte 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 . lists 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.


Credo che 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
plausibile

@MSeifert Scusa per quello, risolto :-). Non lo so davvero, ricordo di averlo trovato in passato ad un certo punto ma non gli ho mai prestato troppa attenzione, forse farò solo una domanda in futuro :-)
Dimitris Fasarakis Hilliard

29

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.


3

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.

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.