A tuple
benötigt in Python weniger Speicherplatz:
>>> a = (1,2,3)
>>> a.__sizeof__()
48
wohingegen list
s mehr Speicherplatz benötigt:
>>> b = [1,2,3]
>>> b.__sizeof__()
64
Was passiert intern in der Python-Speicherverwaltung?
A tuple
benötigt in Python weniger Speicherplatz:
>>> a = (1,2,3)
>>> a.__sizeof__()
48
wohingegen list
s mehr Speicherplatz benötigt:
>>> b = [1,2,3]
>>> b.__sizeof__()
64
Was passiert intern in der Python-Speicherverwaltung?
Antworten:
Ich gehe davon aus, dass Sie CPython und mit 64 Bit verwenden (ich habe die gleichen Ergebnisse auf meinem CPython 2.7 64-Bit erhalten). Es kann Unterschiede bei anderen Python-Implementierungen geben oder wenn Sie ein 32-Bit-Python haben.
Unabhängig von der Implementierung haben list
s eine variable Größe, während tuple
s eine feste Größe haben.
Damit tuple
s die Elemente direkt in der Struktur speichern können, benötigen Listen andererseits eine Indirektionsebene (sie speichert einen Zeiger auf die Elemente). Diese Indirektionsebene ist ein Zeiger auf 64-Bit-Systemen, der 64-Bit ist, also 8 Byte.
Aber es gibt noch eine andere Sache, die sie list
tun: Sie überbeanspruchen. Andernfalls list.append
wäre eine O(n)
Operation immer - um sie amortisiert zu machen O(1)
(viel schneller !!!), wird sie zu viel zugeordnet. Aber jetzt muss es die zugewiesene Größe und die gefüllte Größe verfolgen (es muss tuple
nur eine Größe gespeichert werden, da zugewiesene und gefü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 Bytes.
So list
Bedürfnis mindestens 16 Bytes mehr Speicher als tuple
s. Warum habe ich "zumindest" gesagt? Wegen der Überallokation. Überzuweisung bedeutet, dass mehr Speicherplatz als erforderlich zugewiesen wird. Die Höhe der Überzuweisung hängt jedoch davon ab, wie Sie die Liste und den Verlauf zum Anhängen / Löschen 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
Ich habe beschlossen, einige Bilder zu erstellen, um die obige Erklärung zu begleiten. Vielleicht sind diese hilfreich
So wird es in Ihrem Beispiel (schematisch) im Speicher gespeichert. Ich habe die Unterschiede bei roten (Freihand-) Zyklen hervorgehoben:
int
Dies ist eigentlich nur eine Annäherung, da Objekte auch Python-Objekte sind und CPython sogar kleine Ganzzahlen wiederverwendet. Eine wahrscheinlich genauere Darstellung (obwohl nicht so lesbar) der Objekte im Speicher wäre also:
Nützliche Links:
tuple
Struktur im CPython-Repository für Python 2.7list
Struktur im CPython-Repository für Python 2.7int
Struktur im CPython-Repository für Python 2.7Beachten Sie, dass __sizeof__
die "richtige" Größe nicht wirklich zurückgegeben wird! Es wird nur die Größe der gespeicherten Werte zurückgegeben. Wenn Sie sys.getsizeof
das Ergebnis verwenden, ist es jedoch 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. Diese sind real , das ist der Garbage Collector-Overhead, der in der __sizeof__
Methode nicht berücksichtigt wird. Das liegt daran, dass Sie im Allgemeinen keine magischen Methoden direkt verwenden sollten - verwenden Sie in diesem Fall die Funktionen, die wissen, wie man damit umgeht: sys.getsizeof
(wodurch der GC-Overhead tatsächlich zu dem von zurückgegebenen Wert hinzugefügt wird __sizeof__
).
list
Speicherzuordnung stackoverflow.com/questions/40018398/…
list()
oder ein Listenverständnis erfahren möchten .
Ich werde tiefer in die CPython-Codebasis eintauchen, 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 list
s wird aus der folgenden Funktion berechnet 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 das ob_type
von self
(zurückkehrt PyList_Type
) _PyObject_SIZE
erfasst, während ein anderes Makro tp_basicsize
von diesem Typ erfasst wird . tp_basicsize
wird berechnet als sizeof(PyListObject)
wo PyListObject
ist die Instanzstruktur.
Die PyListObject
Struktur besteht aus drei Feldern:
PyObject_VAR_HEAD # 24 bytes
PyObject **ob_item; # 8 bytes
Py_ssize_t allocated; # 8 bytes
Diese haben Kommentare (die ich gekürzt 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
), so dass ein 24
Byte-Beitrag.
So ist vorerst res
:
sizeof(PyListObject) + self->allocated * sizeof(void*)
oder:
40 + self->allocated * sizeof(void*)
Wenn die Listeninstanz Elemente enthält, die zugewiesen 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 von Listen wie folgt berechnet:
>>> [].__sizeof__()
40
dh die Größe der Instanzstruktur.
tuple
Objekte definieren keine tuple_sizeof
Funktion. Stattdessen object_sizeof
berechnen sie ihre Größe:
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 list
s, erfasst das tp_basicsize
und, wenn das Objekt eine Nicht-Null hat tp_itemsize
(was bedeutet, dass es Instanzen variabler Länge hat), multipliziert es die Anzahl der Elemente im Tupel (über das es erhält Py_SIZE
) mit tp_itemsize
.
tp_basicsize
verwendet wieder, sizeof(PyTupleObject)
wo die PyTupleObject
Struktur enthält :
PyObject_VAR_HEAD # 24 bytes
PyObject *ob_item[1]; # 8 bytes
Ohne Elemente (dh Py_SIZE
Rückgabe 0
) ist die Größe der leeren Tupel also gleich sizeof(PyTupleObject)
:
>>> ().__sizeof__()
24
huh? Nun, hier ist eine Kuriosität, für die ich keine Erklärung gefunden habe. Das tp_basicsize
von tuple
s wird tatsächlich wie folgt berechnet:
sizeof(PyTupleObject) - sizeof(PyObject *)
Warum ein zusätzliches 8
Byte entfernt tp_basicsize
wird, konnte ich nicht herausfinden. (Eine mögliche Erklärung finden Sie im Kommentar von MSeifert.)
Dies ist jedoch im Grunde der Unterschied in Ihrem spezifischen Beispiel . list
s behalten auch eine Reihe von zugewiesenen Elementen bei, um festzustellen, wann eine erneute Zuordnung erforderlich ist.
Wenn nun zusätzliche Elemente hinzugefügt werden, führen Listen diese Überzuweisung tatsächlich durch, um O (1) -Anhänge zu erzielen. Dies führt zu größeren Größen, da MSeiferts Cover in seiner Antwort gut abdeckt.
ob_item[1]
ist meistens ein Platzhalter (daher ist es sinnvoll, ihn von der Basicsize abzuziehen). Das tuple
wird mit zugeordnet PyObject_NewVar
. Ich habe die Details nicht herausgefunden, also ist das nur eine fundierte Vermutung ...
Die Antwort von MSeifert deckt dies weitgehend ab. Um es einfach zu halten, können Sie sich vorstellen:
tuple
ist unveränderlich. Sobald es eingestellt 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änderlich. Sie können Elemente hinzufügen oder daraus entfernen. Es muss die Größe kennen (für interne Impl.). Die Größe wird nach Bedarf geändert.
Es gibt keine kostenlosen Mahlzeiten - diese Funktionen sind mit Kosten verbunden. Daher der Overhead im Speicher für Listen.
Der Größe des Tupels wird ein Präfix vorangestellt, was bedeutet, dass der Interpreter bei der Tupelinitialisierung genügend Speicherplatz für die enthaltenen Daten reserviert. Damit ist das Ende erreicht, da es unveränderlich ist (nicht geändert werden kann), während eine Liste ein veränderliches Objekt ist und somit Dynamik impliziert Zuweisung von Speicher, um zu vermeiden, dass bei jedem Anhängen oder Ändern der Liste Speicherplatz zugewiesen wird (genügend Speicherplatz zuweisen, um die geänderten Daten aufzunehmen und die Daten darauf zu kopieren), wird zusätzlicher Speicherplatz für zukünftige Anhänge, Änderungen usw. zugewiesen fasst es zusammen.