Python verbraucht viel Speicher oder wie kann die Größe von Objekten reduziert werden?

Ein Speicherproblem kann auftreten, wenn während der Ausführung eines Programms eine große Anzahl von Objekten im RAM aktiv ist, insbesondere wenn die Gesamtmenge des verfügbaren Speichers eingeschränkt ist.


Im Folgenden finden Sie eine Übersicht über einige Methoden zum Reduzieren der Größe von Objekten, wodurch der für Programme in reinem Python benötigte RAM-Speicher erheblich reduziert werden kann.


Hinweis: Dies ist die englische Version meines ursprünglichen Beitrags (auf Russisch).


Der Einfachheit halber werden wir Strukturen in Python betrachten, um Punkte mit den Koordinaten x , y , z mit Zugriff auf die Koordinatenwerte nach Namen darzustellen.


Dikt


In kleinen Programmen, insbesondere in Skripten, ist es recht einfach und bequem, das integrierte dict zur Darstellung von Strukturinformationen zu verwenden:


 >>> ob = {'x':1, 'y':2, 'z':3} >>> x = ob['x'] >>> ob['y'] = y 

Mit dem Aufkommen einer kompakteren Implementierung in Python 3.6 mit einem geordneten Schlüsselsatz ist das dict noch attraktiver geworden. Schauen wir uns jedoch die Größe des Footprints im RAM an:


 >>> print(sys.getsizeof(ob)) 240 

Es benötigt viel Speicher, insbesondere wenn Sie plötzlich eine große Anzahl von Instanzen erstellen müssen:


Anzahl der InstanzenGröße der Objekte
1.000.000240 Mb
10.000.0002,40 GB
100.000.00024 gb

Klasseninstanz


Für diejenigen, die alles in Klassen kleiden möchten, ist es vorzuziehen, Strukturen als Klasse mit Zugriff über den Attributnamen zu definieren:


 class Point: # def __init__(self, x, y, z): self.x = x self.y = y self.z = z >>> ob = Point(1,2,3) >>> x = ob.x >>> ob.y = y 

Die Struktur der Klasseninstanz ist interessant:


FeldGröße (Bytes)
PyGC_Head24
PyObject_HEAD16
__weakref__8
__dict__8
GESAMT:56

Hier ist __weakref__ ein Verweis auf die Liste der sogenannten schwachen Verweise auf dieses Objekt, das Feld __dict__ ist ein Verweis auf das Klasseninstanzwörterbuch, das die Werte von __dict__ enthält (beachten Sie, dass die 64-Bit-Referenzplattform 8 Bytes belegt). Ab Python 3.3 werden im gemeinsam genutzten Bereich Schlüssel für alle Instanzen der Klasse im Wörterbuch gespeichert. Dies reduziert die Größe der Instanzablaufverfolgung im RAM:


 >>> print(sys.getsizeof(ob), sys.getsizeof(ob.__dict__)) 56 112 

Infolgedessen hat eine große Anzahl von Klasseninstanzen einen geringeren Speicherbedarf als ein reguläres Wörterbuch ( dict ):


Anzahl der InstanzenGröße
1.000.000168 Mb
10.000.0001,68 GB
100.000.00016,8 Gb

Es ist leicht zu erkennen, dass die Größe der Instanz im RAM aufgrund der Größe des Wörterbuchs der Instanz immer noch groß ist.


Instanz der Klasse mit __slots__


Eine signifikante Reduzierung der Größe einer Klasseninstanz im RAM wird durch Eliminieren von __dict__ und __weakref__ . Dies ist mit Hilfe eines "Tricks" mit __slots__ :


 class Point: __slots__ = 'x', 'y', 'z' def __init__(self, x, y, z): self.x = x self.y = y self.z = z >>> ob = Point(1,2,3) >>> print(sys.getsizeof(ob)) 64 

Die Objektgröße im RAM ist deutlich kleiner geworden:


FeldGröße (Bytes)
PyGC_Head24
PyObject_HEAD16
x8
y8
z8
GESAMT:64

Durch die Verwendung von __slots__ in der Klassendefinition wird der Footprint einer großen Anzahl von Instanzen im Speicher erheblich reduziert:


Anzahl der InstanzenGröße
1.000.00064 Mb
10.000.000640 Mb
100.000.0006,4 GB

Derzeit ist dies die Hauptmethode, um den Speicherbedarf einer Instanz einer Klasse im RAM erheblich zu reduzieren.


Diese Reduzierung wird durch die Tatsache erreicht, dass im Speicher nach dem Titel des Objekts Objektreferenzen gespeichert werden - die Attributwerte, und der Zugriff darauf erfolgt über spezielle Deskriptoren, die sich im Klassenwörterbuch befinden:


 >>> pprint(Point.__dict__) mappingproxy( .................................... 'x': <member 'x' of 'Point' objects>, 'y': <member 'y' of 'Point' objects>, 'z': <member 'z' of 'Point' objects>}) 

Um das Erstellen einer Klasse mit __slots__ zu automatisieren, gibt es eine Bibliothek [namedlist] ( https://pypi.org/project/namedlist ). Die Funktion namedlist.namedlist erstellt eine Klasse mit __slots__ :


 >>> Point = namedlist('Point', ('x', 'y', 'z')) 

Mit einem anderen Paket [attrs] ( https://pypi.org/project/attrs ) können Sie das Erstellen von Klassen mit und ohne __slots__ .


Tupel


Python verfügt außerdem über ein integriertes Typ- tuple zur Darstellung unveränderlicher Datenstrukturen. Ein Tupel ist eine feste Struktur oder ein Datensatz, jedoch ohne Feldnamen. Für den Feldzugriff wird der Feldindex verwendet. Die Tupelfelder sind zum Zeitpunkt der Erstellung der Tupelinstanz ein für alle Mal den Wertobjekten zugeordnet:


 >>> ob = (1,2,3) >>> x = ob[0] >>> ob[1] = y # ERROR 

Beispiele für Tupel sind recht kompakt:


 >>> print(sys.getsizeof(ob)) 72 

Sie belegen 8 Byte mehr Speicher als Instanzen von Klassen mit __slots__ , da der Tupel-Trace im Speicher auch eine Reihe von Feldern enthält:


FeldGröße (Bytes)
PyGC_Head24
PyObject_HEAD16
ob_size8
[0]8
[1]8
[2]8
GESAMT:72

Namedtuple


Da das Tupel sehr häufig verwendet wird, gab es eines Tages die Anfrage, dass Sie weiterhin Zugriff auf die Felder und auch auf den Namen haben könnten. Die Antwort auf diese Anfrage war das Modul collections.namedtuple .


Die Funktion namedtuple soll den Prozess der Generierung solcher Klassen automatisieren:


 >>> Point = namedtuple('Point', ('x', 'y', 'z')) 

Es wird eine Unterklasse von Tupeln erstellt, in der Deskriptoren für den Zugriff auf Felder nach Namen definiert sind. In unserem Beispiel würde es ungefähr so ​​aussehen:


  class Point(tuple): # @property def _get_x(self): return self[0] @property def _get_y(self): return self[1] @property def _get_z(self): return self[2] # def __new__(cls, x, y, z): return tuple.__new__(cls, (x, y, z)) 

Alle Instanzen solcher Klassen haben einen Speicherbedarf, der mit dem eines Tupels identisch ist. Eine große Anzahl von Instanzen hinterlässt einen etwas größeren Speicherbedarf:


Anzahl der InstanzenGröße
1.000.00072 Mb
10.000.000720 Mb
100.000.0007,2 GB

Recordclass: veränderbares Namedtupel ohne zyklische GC


Da die namedtuple und dementsprechend die namedtuple Tupelklassen unveränderliche Objekte in dem Sinne erzeugen, dass das Attribut ob.x keinem anderen Wertobjekt mehr zugeordnet werden kann, ist eine Anforderung für eine veränderbare benannte Tupelvariante entstanden. Da es in Python keinen integrierten Typ gibt, der mit dem Tupel identisch ist, das Zuweisungen unterstützt, wurden viele Optionen erstellt. Wir werden uns auf [recordclass] ( https://pypi.org/project/recordclass ) konzentrieren, das mit [stackoverflow] bewertet wurde ( https://stackoverflow.com/questions/29290359/existence-of-mutable-named- Tupel-in- Python / 29419745). Darüber hinaus kann es verwendet werden, um die Größe von Objekten im RAM im Vergleich zur Größe von tupelähnlichen Objekten zu reduzieren.


Das Paket recordclass führt den Typ recordclass.mutabletuple , der fast identisch mit dem Tupel ist, aber auch Zuweisungen unterstützt. Auf dieser Grundlage werden Unterklassen erstellt, die fast vollständig mit den Namenstupeln identisch sind, aber auch die Zuweisung neuer Werte zu Feldern unterstützen (ohne neue Instanzen zu erstellen). Mit der Funktion namedtuple können Sie wie mit der Funktion namedtuple die Erstellung dieser Klassen automatisieren:


  >>> Point = recordclass('Point', ('x', 'y', 'z')) >>> ob = Point(1, 2, 3) 

PyGC_Head haben dieselbe Struktur wie tuple , jedoch nur ohne PyGC_Head :


FeldGröße (Bytes)
PyObject_HEAD16
ob_size8
x8
y8
y8
GESAMT:48

Standardmäßig erstellt die Funktion recordclass eine Klasse, die nicht am zyklischen recordclass teilnimmt. In der Regel werden namedtuple und recordclass verwendet, um Klassen zu generieren, die Datensätze oder einfache (nicht rekursive) Datenstrukturen darstellen. Wenn Sie sie in Python richtig verwenden, werden keine Zirkelverweise generiert. Aus diesem Grund wird nach Instanzen von Klassen, die von recordclass generiert wurden, default, the PyGC_Head- fragment is excluded, which is necessary for classes supporting the cyclic garbage collection mechanism (more precisely: in the Speicherbereinigung fragment is excluded, which is necessary for classes supporting the cyclic garbage collection mechanism (more precisely: in the PyTypeObject- structure, corresponding to the created class, in the field, by default, the flag flags ist field, by default, the flag Py_TPFLAGS_HAVE_GC` nicht gesetzt.


Die Größe des Speicherbedarfs einer großen Anzahl von Instanzen ist kleiner als die von Instanzen der Klasse mit __slots__ :


Anzahl der InstanzenGröße
1.000.00048 Mb
10.000.000480 Mb
100.000.0004,8 GB

Datenobjekt


Eine andere in der Datensatzklassenbibliothek vorgeschlagene Lösung basiert auf der Idee: Verwenden Sie im Speicher dieselbe Speicherstruktur wie in Klasseninstanzen mit __slots__ , nehmen Sie jedoch nicht am zyklischen Speicherbereinigungsmechanismus teil. Solche Klassen werden mit der Funktion recordclass.make_dataclass generiert:


  >>> Point = make_dataclass('Point', ('x', 'y', 'z')) 

Die auf diese Weise erstellte Klasse erstellt standardmäßig veränderbare Instanzen.


Eine andere Möglichkeit: Verwenden Sie die Klassendeklaration mit Vererbung von recordclass.dataobject :


 class Point(dataobject): x:int y:int z:int 

Auf diese Weise erstellte Klassen erstellen Instanzen, die nicht am zyklischen Speicherbereinigungsmechanismus teilnehmen. Die Struktur der Instanz im Speicher ist dieselbe wie im Fall von __slots__ , jedoch ohne PyGC_Head :


FeldGröße (Bytes)
PyObject_HEAD16
x8
y8
y8
GESAMT:40

 >>> ob = Point(1,2,3) >>> print(sys.getsizeof(ob)) 40 

Um auf die Felder zuzugreifen, werden spezielle Deskriptoren verwendet, um auf das Feld durch seinen Versatz vom Anfang des Objekts zuzugreifen, die sich im Klassenwörterbuch befinden:


 mappingproxy({'__new__': <staticmethod at 0x7f203c4e6be0>, ....................................... 'x': <recordclass.dataobject.dataslotgetset at 0x7f203c55c690>, 'y': <recordclass.dataobject.dataslotgetset at 0x7f203c55c670>, 'z': <recordclass.dataobject.dataslotgetset at 0x7f203c55c410>}) 

Die Größe des Speicherbedarfs einer großen Anzahl von Instanzen ist für CPython das minimal mögliche:


Anzahl der InstanzenGröße
1.000.00040 Mb
10.000.000400 Mb
100.000.0004,0 GB

Cython


Es gibt einen Ansatz, der auf der Verwendung von [Cython] basiert ( https://cython.org ). Sein Vorteil ist, dass die Felder die Werte der Atomtypen der C-Sprache annehmen können. Deskriptoren für den Zugriff auf Felder aus reinem Python werden automatisch erstellt. Zum Beispiel:


 cdef class Python: cdef public int x, y, z def __init__(self, x, y, z): self.x = x self.y = y self.z = z 

In diesem Fall haben die Instanzen eine noch kleinere Speichergröße:


 >>> ob = Point(1,2,3) >>> print(sys.getsizeof(ob)) 32 

Die Instanzablaufverfolgung im Speicher hat die folgende Struktur:


FeldGröße (Bytes)
PyObject_HEAD16
x4
y4
y4
ist leer4
GESAMT:32

Die Größe des Footprints einer großen Anzahl von Kopien ist geringer:


NummerGröße
1.000.00032 Mb
10.000.000320 Mb
100.000.0003,2 GB

Es ist jedoch zu beachten, dass beim Zugriff von Python-Code jedes Mal eine Konvertierung von int in ein Python-Objekt und umgekehrt durchgeführt wird.


Numpy


Die Verwendung mehrdimensionaler Arrays oder Arrays von Datensätzen für eine große Datenmenge führt zu einem Speichergewinn. Für eine effiziente Verarbeitung in reinem Python sollten Sie jedoch Verarbeitungsmethoden verwenden, die sich auf die Verwendung von Funktionen aus dem numpy Paket konzentrieren.


 >>> Point = numpy.dtype(('x', numpy.int32), ('y', numpy.int32), ('z', numpy.int32)]) 

Ein Array von N Elementen, die mit Nullen initialisiert sind, wird mit der folgenden Funktion erstellt:


  >>> points = numpy.zeros(N, dtype=Point) 

Die Größe des Arrays im Speicher ist so gering wie möglich:


Anzahl der ObjekteGröße
1.000.00012 Mb
10.000.000120 Mb
100.000.0001,20 GB

Der normale Zugriff auf Array-Elemente und Zeilen erfordert die Konvertierung von einem Python-Objekt in einen C int -Wert und umgekehrt. Das Extrahieren einer einzelnen Zeile führt zur Erstellung eines Arrays, das ein einzelnes Element enthält. Seine Spur wird nicht mehr so ​​kompakt sein:


  >>> sys.getsizeof(points[0]) 68 

Wie oben erwähnt, ist es daher im Python-Code erforderlich, Arrays mithilfe von Funktionen aus dem numpy Paket zu verarbeiten.


Fazit


An einem klaren und einfachen Beispiel konnte überprüft werden, ob die Python-Programmiersprachen-Community (CPython) aus Entwicklern und Benutzern echte Möglichkeiten für eine signifikante Reduzierung des von Objekten verwendeten Arbeitsspeichers bietet.

Source: https://habr.com/ru/post/de458518/


All Articles