Warum nehmen Tupel weniger Speicherplatz ein als Listen?


105

A tuplebenötigt in Python weniger Speicherplatz:

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

wohingegen lists mehr Speicherplatz benötigt:

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

Was passiert intern in der Python-Speicherverwaltung?


1
Ich bin nicht sicher, wie dies intern funktioniert, aber das Listenobjekt hat zumindest mehr Funktionen wie zum Beispiel Anhängen, die das Tupel nicht hat. Es ist daher sinnvoll, das Tupel als einfacheren Objekttyp kleiner zu machen
Metareven

Ich denke, es hängt auch von Maschine zu Maschine ab ... für mich, wenn ich überprüfe, dass a = (1,2,3) 72 und b = [1,2,3] 88 dauert.
Amrit

6
Python-Tupel sind unveränderlich. Veränderbare Objekte haben zusätzlichen Overhead, um Laufzeitänderungen zu verarbeiten.
Lee Daniel Crocker

@Metareven Die Anzahl der Methoden eines Typs hat keinen Einfluss auf den Speicherplatz, den die Instanzen belegen. Die Methodenliste und ihr Code werden vom Objektprototyp verarbeitet, Instanzen speichern jedoch nur Daten und interne Variablen.
Jjmontes

Antworten:


144

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 lists eine variable Größe, während tuples eine feste Größe haben.

Damit tuples 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 listtun: Sie überbeanspruchen. Andernfalls list.appendwä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 tuplenur 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 listBedürfnis mindestens 16 Bytes mehr Speicher als tuples. 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

Bilder

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:

Geben Sie hier die Bildbeschreibung ein

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

Geben Sie hier die Bildbeschreibung ein

Nützliche Links:

Beachten 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.getsizeofdas 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__).


Betreff " Listen benötigen also mindestens 16 Byte mehr Speicher als Tupel. ", Wäre das nicht 8? Eine Größe für Tupel und zwei Größen für Listen bedeuten eine zusätzliche Größe für Listen.
Ikegami

1
Ja, die Liste hat eine zusätzliche "Größe" (8 Byte), speichert aber auch einen Zeiger (8 Byte) auf das "Array von PyObjects", anstatt sie direkt in der Struktur zu speichern (was das Tupel tut). 8 + 8 = 16.
MSeifert

2
Ein weiterer nützlicher Link zur listSpeicherzuordnung stackoverflow.com/questions/40018398/…
vishes_shell

@vishes_shell Das hängt nicht wirklich mit der Frage zusammen, da der Code in der Frage überhaupt nicht überbelegt ist . Aber ja, es ist nützlich, wenn Sie mehr über das Ausmaß der Überzuweisung bei der Verwendung list()oder ein Listenverständnis erfahren möchten .
MSeifert

1
@ user3349993 Tupel sind unveränderlich, sodass Sie kein Tupel anhängen oder ein Element aus einem Tupel entfernen können.
MSeifert

31

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 lists 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_typevon self(zurückkehrt PyList_Type) _PyObject_SIZEerfasst, während ein anderes Makro tp_basicsizevon diesem Typ erfasst wird . tp_basicsizewird berechnet als sizeof(PyListObject)wo PyListObjectist die Instanzstruktur.

Die PyListObjectStruktur 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_HEADerweitert sich in drei 8-Byte-Felder ( ob_refcount, ob_typeund ob_size), so dass ein 24Byte-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->allocatedenthä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.


tupleObjekte definieren keine tuple_sizeofFunktion. Stattdessen object_sizeofberechnen 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 lists, erfasst das tp_basicsizeund, 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_basicsizeverwendet wieder, sizeof(PyTupleObject)wo die PyTupleObjectStruktur enthält :

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

Ohne Elemente (dh Py_SIZERü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_basicsizevon tuples wird tatsächlich wie folgt berechnet:

sizeof(PyTupleObject) - sizeof(PyObject *)

Warum ein zusätzliches 8Byte entfernt tp_basicsizewird, 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 . lists 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.


Ich glaube, das ob_item[1]ist meistens ein Platzhalter (daher ist es sinnvoll, ihn von der Basicsize abzuziehen). Das tuplewird mit zugeordnet PyObject_NewVar. Ich habe die Details nicht herausgefunden, also ist das nur eine fundierte Vermutung ...
MSeifert

@MSeifert Entschuldigung, behoben :-). Ich weiß es wirklich nicht, ich erinnere mich, dass ich es irgendwann in der Vergangenheit gefunden habe, aber ich habe ihm nie zu viel Aufmerksamkeit geschenkt, vielleicht stelle ich irgendwann in der Zukunft nur eine Frage :-)
Dimitris Fasarakis Hilliard

29

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

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

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


3

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.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.