Jede Instanz einer Klasse in CPython, die mit der Klassensyntax erstellt wurde, ist an einem zirkulären Garbage Collection- Mechanismus beteiligt. Dies erhöht den Speicherbedarf jeder Instanz und kann in stark ausgelasteten Systemen zu Speicherproblemen führen .
Ist es bei Bedarf möglich, auf einen grundlegenden Linkzählmechanismus zu verzichten?
Lassen Sie uns einen Ansatz analysieren, mit dessen Hilfe Klassen erstellt werden können, deren Instanzen nur mithilfe des Linkzählmechanismus gelöscht werden.
Ein bisschen über die Speicherbereinigung in CPython
Der Hauptmechanismus für die Speicherbereinigung in Python ist der Linkzählmechanismus. Jedes Objekt enthält ein Feld, das den aktuellen Wert der Anzahl der Verknüpfungen enthält. Ein Objekt wird zerstört, sobald der Wert des Referenzzählers Null wird. Es ist jedoch nicht möglich, Objekte zu entsorgen, die Zirkelverweise enthalten. Zum Beispiel
lst = [] lst.append(lst) del lst
In solchen Fällen bleibt der Verweisungszähler nach dem Löschen des Objekts größer als Null. Um dieses Problem zu lösen, verfügt Python über einen zusätzlichen Mechanismus, der Objekte verfolgt und Schleifen im Verknüpfungsdiagramm zwischen Objekten unterbricht. Es gibt einen guten Artikel darüber, wie der Garbage Collection-Mechanismus in CPython3 funktioniert.
Gemeinkosten im Zusammenhang mit dem Speicherbereinigungsmechanismus
Normalerweise verursacht der Speicherbereinigungsmechanismus keine Probleme. Damit sind jedoch bestimmte Gemeinkosten verbunden:
Wenn jede Speicherklasse zugewiesen ist, wird der PyGC_Head- Header hinzugefügt : (mindestens 24 Byte in Python <= 3,7 und mindestens 16 Byte in 3,8 auf einer 64-Bit-Plattform.
Dies kann zu einem Problem mit Speichermangel führen, wenn Sie viele Instanzen desselben Prozesses ausführen, in denen Sie eine sehr große Anzahl von Objekten mit einer relativ kleinen Anzahl von Attributen benötigen und die Speichergröße begrenzt ist.
Ist es manchmal möglich, sich auf den grundlegenden Mechanismus der Linkzählung zu beschränken?
Der Mechanismus der zirkulären Speicherbereinigung kann redundant sein, wenn die Klasse einen nicht rekursiven Datentyp darstellt. Zum Beispiel Datensätze, die Werte eines einfachen Typs enthalten (Zahlen, Zeichenfolgen, Datum / Uhrzeit). Betrachten Sie zur Veranschaulichung eine einfache Klasse:
class Point: x: int y: int
Bei korrekter Verwendung sind Verbindungszyklen nicht möglich. Obwohl in Python nichts daran hindert, sich in den Fuß zu treten:
p = Point(0, 0) px = p
Für die Point
Klasse könnte man sich jedoch auf einen Link-Zählmechanismus beschränken. Es gibt jedoch noch keinen Standardmechanismus zum Ablehnen der zyklischen Speicherbereinigung für eine einzelne Klasse.
Modernes CPython ist so konzipiert, dass beim Definieren benutzerdefinierter Klassen in der Struktur , die für den Typ verantwortlich ist, der die benutzerdefinierte Klasse definiert, immer das Flag Py_TPFLAGS_HAVE_GC gesetzt wird. Es bestimmt, dass Klasseninstanzen in den Garbage Collection-Mechanismus aufgenommen werden. Für alle diese Objekte wird beim Erstellen der Header PyGC_Head hinzugefügt und sie werden in die Liste der überwachten Objekte aufgenommen. Wenn das Flag Py_TPFLAGS_HAVE_GC
nicht gesetzt ist, funktioniert nur der grundlegende Py_TPFLAGS_HAVE_GC
. Ein einmaliges Zurücksetzen von Py_TPFLAGS_HAVE_GC
jedoch nicht. Sie müssen Änderungen am CPython-Kern vornehmen, der für das Erstellen und Zerstören von Instanzen verantwortlich ist. Und das ist immer noch problematisch.
Über eine Implementierung
Betrachten Sie als Beispiel für die Implementierung der Idee das dataobject
der Basisklasse aus dem Recordclass- Projekt. Mithilfe dieser Funktion können Sie Klassen erstellen, deren Instanzen nicht am Mechanismus der zirkulären Py_TPFLAGS_HAVE_GC
( Py_TPFLAGS_HAVE_GC
nicht installiert und dementsprechend gibt es keinen zusätzlichen PyGC_Head
Header). Sie haben genau die gleiche Struktur im Speicher wie Klasseninstanzen mit __slots__ , jedoch ohne PyGC_Head
:
from recordclass import dataobject class Point(dataobject): x:int y:int >>> p = Point(1,2) >>> print(p.__sizeof__(), sys.getsizeof(p)) 32 32
Zum Vergleich geben wir eine ähnliche Klasse mit __slots__
:
class Point: __slots__ = 'x', 'y' x:int y:int >>> p = Point(1,2) >>> print(p.__sizeof__(), sys.getsizeof(p)) 32 64
Der Größenunterschied entspricht genau der Größe des PyGC_Head
Headers. In Fällen mit mehreren Attributen kann eine solche Erhöhung der Größe der Ablaufverfolgung im RAM erheblich sein. Bei Instanzen der Point
Klasse wird durch Hinzufügen von PyGC_Head
die Größe um das PyGC_Head
erhöht.
Um diesen Effekt zu erzielen, wird ein spezieller Metaklassendatentyp datatype
, der die Einstellung von Unterklassen von dataobject
. Infolge der Konfiguration wird das Py_TPFLAGS_HAVE_GC
Flag Py_TPFLAGS_HAVE_GC
, und die Basisgröße der tp_basicsize- Instanz erhöht sich um den Betrag, der zum Speichern zusätzlicher Feldsteckplätze erforderlich ist. Die entsprechenden Feldnamen werden aufgelistet, wenn die Klasse deklariert wird (die Point
Klasse hat zwei: x
und y
). Der datatype
Metatlass" bietet auch die Möglichkeit, die Werte der Slots " tp_alloc" , " tp_new" , " tp_dealloc" und " tp_free" festzulegen , die die richtigen Algorithmen zum Erstellen und Zerstören von Instanzen im Speicher implementieren. Standardmäßig fehlen Instanzen __weakref__ und __dict__ (sowie Instanzen von Klassen mit __slots__
).
Fazit
Wie man sehen kann, ist es in CPython bei Bedarf möglich, den Mechanismus der zyklischen Speicherbereinigung für eine bestimmte Klasse zu deaktivieren, wenn die Gewissheit besteht, dass ihre Instanzen keine zirkulären Verknüpfungen bilden. Dadurch wird die Ablaufverfolgung im Speicher um die Größe des PyGC_Head
Headers PyGC_Head
.