web-dev-qa-db-de.com

Warum belegen Tupel weniger Speicherplatz als Listen?

Ein Tuple benötigt weniger Speicherplatz in Python:

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

während lists mehr Speicherplatz beansprucht:

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

Was passiert intern auf der Speicherverwaltung von Python?

99
JON

Ich gehe davon aus, dass Sie CPython und mit 64 Bit verwenden (auf meinem CPython 2.7 64 Bit habe ich die gleichen Ergebnisse erzielt). Es könnte Unterschiede in anderen Python Implementierungen geben oder wenn Sie ein 32-Bit-Python haben.

Unabhängig von der Implementierung haben lists eine variable Größe, während Tuples eine feste Größe haben.

Tuples kann also die Elemente direkt in der Struktur speichern, Listen benötigen dagegen eine Indirektionsebene (sie speichert einen Zeiger auf die Elemente). Diese Indirektionsebene ist ein Zeiger auf 64-Bit-Systemen, die 64-Bit sind, also 8 Byte.

Aber es gibt eine andere Sache, die lists tun: Sie überbelegen. Andernfalls wäre list.append Eine O(n) -Operation immer - um sie amortisiert zu machen O(1) ( viel schneller !!!) es überbelegt. Aber jetzt muss es die zugewiesene Größe und die gefüllte Größe verfolgen Größe (Tuples müssen nur eine Größe speichern, da zugewiesene und ausgefüllte Größe immer identisch sind). Das bedeutet, dass jede Liste eine andere "Größe" speichern muss, die auf 64-Bit-Systemen eine 64-Bit-Ganzzahl ist, wiederum 8 Byte.

Daher benötigen lists mindestens 16 Byte mehr Speicher als Tuples. Warum habe ich "mindestens" gesagt? Wegen der Überbelegung. Überzuweisung bedeutet, dass mehr Speicherplatz zugewiesen wird, als benötigt wird. Die Höhe der Überbelegung hängt jedoch davon ab, "wie" Sie die Liste und den Anhangs-/Löschverlauf erstellen:

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

Bilder

Ich habe mich dazu entschlossen, ein paar Bilder zu erstellen, die der obigen Erklärung beiliegen. Vielleicht sind diese hilfreich

So wird es in Ihrem Beispiel (schematisch) gespeichert. Ich habe die Unterschiede mit roten (Freihand-) Zyklen hervorgehoben:

enter image description here

Das ist eigentlich nur eine Annäherung, weil int Objekte auch Python Objekte sind und CPython sogar kleine Ganzzahlen wiederverwendet, also eine wahrscheinlich genauere Darstellung (obwohl nicht so lesbar) der Objekte im Speicher wäre:

enter image description here

Nützliche Links:

Beachten Sie, dass __sizeof__ Nicht wirklich die "richtige" Größe zurückgibt! Es wird nur die Größe der gespeicherten Werte zurückgegeben. Wenn Sie jedoch sys.getsizeof verwenden, ist das Ergebnis anders:

>>> import sys
>>> l = [1,2,3]
>>> t = (1, 2, 3)
>>> sys.getsizeof(l)
88
>>> sys.getsizeof(t)
72

Es gibt 24 "zusätzliche" Bytes. Dies sind echte , das ist der Garbage-Collector-Overhead, der in der Methode __sizeof__ Nicht berücksichtigt wird. Das liegt daran, dass Sie im Allgemeinen keine magischen Methoden direkt anwenden sollten - verwenden Sie in diesem Fall die Funktionen, die mit ihnen umgehen können: sys.getsizeof (die eigentlich fügt das hinzu GC-Aufwand auf den von __sizeof__ Zurückgegebenen Wert).

135
MSeifert

Ich werde einen tieferen Einblick in die CPython-Codebasis nehmen, damit wir sehen können, wie die Größen tatsächlich berechnet werden. In Ihrem speziellen Beispiel wurden keine Überzuweisungen vorgenommen, daher werde ich darauf nicht eingehen .

Ich werde hier 64-Bit-Werte verwenden, so wie Sie es sind.


Die Größe für lists berechnet sich aus der folgenden Funktion 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);
}

Hier Py_TYPE(self) ist ein Makro, das den ob_type Von self erfasst (PyList_Type Zurückgibt), während _PyObject_SIZE ist ein weiteres Makro, das tp_basicsize von diesem Typ aufnimmt. tp_basicsize Wird berechnet als sizeof(PyListObject) wobei PyListObject die Instanzstruktur ist.

Die PyListObject Struktur hat drei Felder:

PyObject_VAR_HEAD     # 24 bytes 
PyObject **ob_item;   #  8 bytes
Py_ssize_t allocated; #  8 bytes

diese haben Kommentare (die ich abgeschnitten habe), die erklären, was sie sind. Folgen Sie dem obigen Link, um sie zu lesen. PyObject_VAR_HEAD erweitert sich in drei 8-Byte-Felder (ob_refcount, ob_type Und ob_size), Also ein 24 - Byte Beitrag.

Im Moment ist res:

sizeof(PyListObject) + self->allocated * sizeof(void*)

oder:

40 + self->allocated * sizeof(void*)

Wenn die Listeninstanz Elemente enthält, die zugeordnet sind. Der zweite Teil berechnet ihren Beitrag. self->allocated Enthält, wie der Name schon sagt, die Anzahl der zugewiesenen Elemente.

Ohne Elemente wird die Größe der Listen wie folgt berechnet:

>>> [].__sizeof__()
40

die Größe der Instanz struct.


Tuple Objekte definieren keine Tuple_sizeof Funktion. Stattdessen verwenden sie object_sizeof , um ihre Größe zu berechnen:

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);
}

Dies, wie für lists, erfasst den tp_basicsize Und multipliziert, wenn das Objekt einen Wert ungleich Null tp_itemsize Hat (was bedeutet, dass es Instanzen variabler Länge hat), die Anzahl von Elemente im Tupel (die es über Py_SIZE erhält) mit tp_itemsize.

tp_basicsize Verwendet erneut sizeof(PyTupleObject), wobei PyTupleObject struct enthält :

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

Ohne Elemente (das heißt, Py_SIZE Gibt 0 Zurück) ist die Größe leerer Tupel gleich sizeof(PyTupleObject):

>>> ().__sizeof__()
24

huh? Nun, hier ist eine Kuriosität, für die ich keine Erklärung gefunden habe: Der tp_basicsize Von Tuples wird tatsächlich wie folgt berechnet:

sizeof(PyTupleObject) - sizeof(PyObject *)

warum ein zusätzliches 8 Byte aus tp_basicsize entfernt wird, konnte ich nicht herausfinden. (Siehe MSeiferts Kommentar für eine mögliche Erklärung)


Dies ist jedoch im Grunde der Unterschied in Ihrem speziellen Beispiel . lists bewahrt auch eine Reihe von zugewiesenen Elementen auf, um festzustellen, wann eine erneute Zuweisung erforderlich ist.

Wenn nun zusätzliche Elemente hinzugefügt werden, führen Listen tatsächlich diese Überzuweisung durch, um O(1) anzufügen. Dies führt zu größeren Größen, als MSeiferts Cover in seiner Antwort gut aussehen.

Die Antwort von MSeifert deckt dies weitgehend ab. Um es einfach zu halten, können Sie sich vorstellen:

Tuple ist unveränderlich. Sobald es festgelegt ist, können Sie es nicht mehr ändern. Sie wissen also im Voraus, wie viel Speicher Sie für dieses Objekt reservieren müssen.

list ist veränderbar. Sie können Elemente hinzufügen oder entfernen. Die Größe muss bekannt sein (für interne Geräte). Die Größe wird nach Bedarf geändert.

Es gibt keine kostenlosen Mahlzeiten - diese Funktionen sind kostenpflichtig. Daher der Speicheraufwand für Listen.

29
Chen A.

Der Größe des Tupels wird ein Präfix vorangestellt, was bedeutet, dass der Interpreter bei der Tupel-Initialisierung genügend Platz für die enthaltenen Daten reserviert, und das ist das Ende davon, da es unveränderlich ist (nicht geändert werden kann), wohingegen eine Liste ein veränderbares Objekt ist, das daher Dynamik impliziert Zuweisung von Speicher, um zu vermeiden, dass jedes Mal Speicherplatz zugewiesen wird, wenn Sie die Liste anhängen oder ändern (genug Speicherplatz zuweisen, um die geänderten Daten aufzunehmen und die Daten dorthin zu kopieren), wird zusätzlicher Speicherplatz für zukünftige Anhänge, Änderungen usw. zugewiesen fasst es zusammen.

3