Verstehen, wie viel Speicher Ihre Python-Objekte verwenden

Python ist eine fantastische Programmiersprache. Es ist auch dafür bekannt, dass es ziemlich langsam ist, hauptsächlich aufgrund seiner enormen Flexibilität und dynamischen Eigenschaften. Für viele Anwendungen und Domänen ist dies aufgrund ihrer Anforderungen und verschiedenen Optimierungstechniken kein Problem. Es ist weniger bekannt, dass Python-Objektgraphen (verschachtelte Wörterbücher für Listen und Tupel sowie einfache Typen) viel Speicherplatz beanspruchen. Dies kann ein viel schwerwiegenderer Begrenzungsfaktor sein, da er Auswirkungen auf das Caching, den virtuellen Speicher, die Mandantenfähigkeit mit anderen Programmen hat und generell den verfügbaren Speicher schneller erschöpft, was eine knappe und teure Ressource ist.

Es stellt sich heraus, dass es nicht trivial ist, herauszufinden, wie viel Speicher tatsächlich verbraucht wird. In diesem Artikel werde ich Sie durch die Feinheiten der Speicherverwaltung von Python-Objekten führen und zeigen, wie Sie den verbrauchten Speicher genau messen können.

In diesem Artikel konzentriere ich mich ausschließlich auf CPython, die primäre Implementierung der Programmiersprache Python. Die Experimente und Schlussfolgerungen hier gelten nicht für andere Python-Implementierungen wie IronPython, Jython und PyPy.

Ich habe auch die Zahlen auf 64-Bit-Python 2.7 ausgeführt. In Python 3 sind die Zahlen manchmal etwas unterschiedlich (insbesondere für Zeichenfolgen, die immer Unicode sind), die Konzepte sind jedoch die gleichen.

Praktische Erkundung der Python-Speichernutzung

Lassen Sie uns zunächst ein wenig erforschen und ein konkretes Bild der tatsächlichen Speichernutzung von Python-Objekten erhalten.

Die integrierte sys.getsizeof () - Funktion

Das sys-Modul der Standardbibliothek stellt die Funktion getsizeof () bereit. Diese Funktion akzeptiert ein Objekt (und eine optionale Standardeinstellung) und ruft das Objekt auf Größe von() -Methode und gibt das Ergebnis zurück, damit Sie Ihre Objekte auch inspizierbar machen können.

Messen des Speichers von Python-Objekten

Beginnen wir mit einigen numerischen Typen:

"python import sys

sys.getsizeof (5) 24 "

Interessant. Eine ganze Zahl benötigt 24 Bytes.

python sys.getsizeof (5.3) 24

Hmm… ein Float braucht auch 24 Bytes.

python from decimal import Decimal sys.getsizeof (Decimal (5.3)) 80

Beeindruckend. 80 Bytes! Dies lässt Sie wirklich darüber nachdenken, ob Sie eine große Anzahl reeller Zahlen als Floats oder Dezimalzahlen darstellen möchten.

Gehen wir weiter zu Strings und Kollektionen:

"python sys.getsizeof (") 37 sys.getsizeof ('1') 38 sys.getsizeof ('1234') 41

sys.getsizeof (u ") 50 sys.getsizeof (u'1 ') 52 sys.getsizeof (u'1234') 58"

OK. Eine leere Zeichenfolge benötigt 37 Byte, und jedes weitere Zeichen fügt ein weiteres Byte hinzu. Das sagt viel über den Kompromiss aus, mehrere kurze Zeichenketten zu behalten, bei denen Sie den 37-Byte-Overhead für jede einzelne bezahlen, gegenüber einer einzelnen langen Zeichenfolge, bei der Sie den Aufwand nur einmal bezahlen.

Unicode-Zeichenfolgen verhalten sich ähnlich, außer dass der Overhead 50 Byte beträgt und jedes zusätzliche Zeichen 2 Byte hinzufügt. Dies ist zu beachten, wenn Sie Bibliotheken verwenden, die Unicode-Zeichenfolgen zurückgeben. Der Text kann jedoch als einfache Zeichenfolgen dargestellt werden.

In Python 3 sind Strings übrigens immer Unicode und der Overhead beträgt 49 Bytes (sie haben irgendwo ein Byte gespeichert). Das Byteobjekt hat einen Overhead von nur 33 Byte. Wenn Sie über ein Programm verfügen, das viele kurze Zeichenfolgen im Arbeitsspeicher verarbeitet und Sie Wert auf die Leistung legen, sollten Sie Python 3 in Betracht ziehen.

python sys.getsizeof ([]) 72 sys.getsizeof ([1]) 88 sys.getsizeof ([1, 2, 3, 4]) 104 sys.getsizeof (['eine lange langgestreckte Zeichenfolge'])

Was ist los? Eine leere Liste benötigt 72 Bytes, aber jedes weitere int addiert nur 8 Bytes, wobei die Größe eines int 24 Bytes beträgt. Eine Liste, die eine lange Zeichenfolge enthält, benötigt nur 80 Byte.

Die Antwort ist einfach. Die Liste enthält nicht die int-Objekte selbst. Es enthält lediglich einen 8-Byte-Zeiger (auf 64-Bit-Versionen von CPython) auf das eigentliche int-Objekt. Das bedeutet, dass die Funktion getsizeof () nicht den tatsächlichen Speicher der Liste und alle darin enthaltenen Objekte zurückgibt, sondern nur den Speicher der Liste und die Zeiger auf ihre Objekte. Im nächsten Abschnitt werde ich die deep_getsizeof () - Funktion vorstellen, die dieses Problem anspricht.

python sys.getsizeof (()) 56 sys.getsizeof ((1,)) 64 sys.getsizeof ((1, 2, 3, 4)) 88 sys.getsizeof (('eine lange, lange Zeichenfolge')) 64

Die Geschichte ist für Tupel ähnlich. Der Overhead eines leeren Tupels beträgt 56 Bytes gegenüber den 72 einer Liste. Dieser Unterschied von 16 Bytes pro Sequenz ist niedrig, wenn Sie eine Datenstruktur mit vielen kleinen, unveränderlichen Sequenzen haben.

"python sys.getsizeof (set ()) 232 sys.getsizeof (set ([1)) 232 sys.getsizeof (set ([1, 2, 3, 4])) 232

sys.getsizeof () 280 sys.getsizeof (dict (a = 1)) 280 sys.getsizeof (dict (a = 1, b = 2, c = 3)) 280 "

Sets und Wörterbücher werden beim Hinzufügen von Elementen anscheinend nicht größer, beachten Sie jedoch den enormen Aufwand.

Die Quintessenz ist, dass Python-Objekte einen großen festen Overhead haben. Wenn Ihre Datenstruktur aus einer großen Anzahl von Sammlungsobjekten wie Strings, Listen und Wörterbüchern besteht, die jeweils eine geringe Anzahl von Elementen enthalten, zahlen Sie eine hohe Gebühr.

Die Funktion deep_getsizeof ()

Nun, da ich Sie halb zu Tode erschreckt habe und auch gezeigt habe, dass sys.getsizeof () Ihnen nur sagen kann, wie viel Speicher ein primitives Objekt benötigt, werfen wir einen Blick auf eine angemessenere Lösung. Die Funktion deep_getsizeof () führt einen rekursiven Drilldown durch und berechnet die tatsächliche Speichernutzung eines Python-Objektdiagramms.

"Python aus Sammlungen importieren Mapping, Container aus sys import getsizeof

def deep_getsizeof (o, ids): "" "Ermitteln Sie den Speicherbedarf eines Python-Objekts

Hierbei handelt es sich um eine rekursive Funktion, die einen Python-Objektgraph wie ein Wörterbuch ausführt, das verschachtelte Wörterbücher mit Listen von Listen, Tupeln und Mengen enthält. Die Funktion sys.getsizeof hat nur eine geringe Größe. Es zählt jedes Objekt innerhalb eines Containers nur als Zeiger, unabhängig davon, wie groß es wirklich ist. : param o: das objekt: param ids:: return: "" "d = deep_getsizeof if id (o) in ids: return 0 r = getsizeof (o) ids.add (id (o)) wenn instanz (o, str ) oder isinstance (0, unicode): return r wenn isinstance (o, Mapping): return r + summe (d (k, ids) + d (v, ids) für k, v in o.iteritems ()), falls es Instanz ist (o, Container): Rückgabe von r + Summe (d (x, ids) für x in o) Rückgabe von r "

Diese Funktion hat mehrere interessante Aspekte. Es berücksichtigt Objekte, die mehrfach referenziert werden, und zählt sie nur einmal, indem sie die Objekt-IDs verfolgt. Das andere interessante Merkmal der Implementierung ist, dass sie die abstrakten Basisklassen des Collections-Moduls voll ausnutzt. Auf diese Weise kann die Funktion alle Sammlungen, die entweder die Mapping- oder Container-Basisklassen implementieren, sehr knapp handhaben, anstatt sich mit Myriaden-Auflistungstypen wie String, Unicode, Bytes, Liste, Tupel, Dict, Frozendict, OrderedDict, Set, Frozenset usw. zu befassen.

Lass es uns in Aktion sehen:

python x = '1234567' deep_getsizeof (x, set ()) 44

Eine Zeichenfolge der Länge 7 benötigt 44 Bytes (37 Overhead + 7 Bytes für jedes Zeichen)..

python deep_getsizeof ([], set ()) 72

Eine leere Liste benötigt 72 Bytes (nur Overhead).

python deep_getsizeof ([x], set ()) 124

Eine Liste mit dem String x benötigt 124 Bytes (72 + 8 + 44)..

python deep_getsizeof ([x, x, x, x, x], set ()) 156

Eine Liste, die 5 mal den String enthält, benötigt 156 Bytes (72 + 5 * 8 + 44)..

Das letzte Beispiel zeigt, dass deep_getsizeof () nur einmal Referenzen auf dasselbe Objekt (die x-Zeichenfolge) zählt, der Zeiger der einzelnen Referenzen jedoch gezählt wird.

Leckereien oder Tricks

Es stellt sich heraus, dass CPython mehrere Tricks im Ärmel hat, so dass die Zahlen, die Sie von deep_getsizeof () erhalten, die Speicherauslastung eines Python-Programms nicht vollständig darstellen.

Referenzzählung

Python verwaltet den Speicher mithilfe von Referenzzählsemantik. Sobald ein Objekt nicht mehr referenziert wird, wird sein Speicher freigegeben. Solange es einen Verweis gibt, wird das Objekt nicht freigegeben. Dinge wie zyklische Referenzen können Sie ziemlich hart beißen.

Kleine Objekte

CPython verwaltet kleine Objekte (weniger als 256 Byte) in speziellen Pools an 8-Byte-Grenzen. Es gibt Pools für 1-8 Bytes, 9-16 Bytes und bis zu 249-256 Bytes. Wenn ein Objekt der Größe 10 zugewiesen wird, wird es aus dem 16-Byte-Pool für Objekte mit einer Größe von 9-16 Byte zugewiesen. Obwohl es nur 10 Bytes Daten enthält, kostet es 16 Bytes Speicher. Wenn Sie 1.000.000 Objekte der Größe 10 zuweisen, verwenden Sie tatsächlich 16.000.000 Bytes und nicht 10.000.000 Bytes, wie Sie vielleicht annehmen. Dieser 60% -ige Overhead ist offensichtlich nicht trivial.

Ganzzahlen

CPython führt eine globale Liste aller ganzen Zahlen im Bereich [-5, 256]. Diese Optimierungsstrategie ist sinnvoll, da kleine Ganzzahlen überall auftauchen und da jede ganze Zahl 24 Byte umfasst, viel Speicher für ein typisches Programm eingespart wird.

Dies bedeutet auch, dass CPython für alle diese Ganzzahlen 266 * 24 = 6384 Byte vorab reserviert, auch wenn Sie die meisten nicht verwenden. Sie können dies überprüfen, indem Sie die Funktion id () verwenden, die den Zeiger auf das tatsächliche Objekt gibt. Wenn Sie id (x) multiple für ein beliebiges x im Bereich [-5, 256] aufrufen, erhalten Sie jedes Mal dasselbe Ergebnis (für dieselbe Ganzzahl). Wenn Sie es jedoch für Ganzzahlen außerhalb dieses Bereichs versuchen, wird jede davon verschieden sein (jedes Mal wird ein neues Objekt erstellt)..

Hier einige Beispiele aus dem Bereich:

"Python-ID (-3) 140251817361752

id (-3) 140251817361752

id (-3) 140251817361752

id (201) 140251817366736

id (201) 140251817366736

ID (201) 140251817366736 "

Hier einige Beispiele außerhalb des Bereichs:

"Python-ID (301) 140251846945800

ID (301) 140251846945776

id (-6) 140251846946960

id (-6) 140251846946936 "

Python-Speicher vs. Systemspeicher

CPython ist irgendwie besitzergreifend. In vielen Fällen werden Speicherobjekte in Ihrem Programm nicht mehr referenziert nicht an das System zurückgegeben werden (z. B. die kleinen Objekte). Dies ist gut für Ihr Programm, wenn Sie viele Objekte zuordnen und freigeben (die zu demselben 8-Byte-Pool gehören), da Python das System nicht stören muss, was relativ teuer ist. Es ist jedoch nicht so toll, wenn Ihr Programm normalerweise X-Bytes verwendet und unter bestimmten Bedingungen 100-mal so viel benötigt (z. B. das Analysieren und Verarbeiten einer großen Konfigurationsdatei erst beim Start)..

Nun kann dieser 100X-Speicher in Ihrem Programm nutzlos eingeschlossen werden, um nie wieder verwendet zu werden und das System daran zu hindern, es anderen Programmen zuzuordnen. Die Ironie ist, dass Sie die Anzahl der Instanzen, die Sie auf einem bestimmten Rechner ausführen können, stark einschränken, wenn Sie das Verarbeitungsmodul verwenden, um mehrere Instanzen Ihres Programms auszuführen.

Speicherprofiler

Um den tatsächlichen Speicherbedarf Ihres Programms zu messen und zu messen, können Sie das Modul memory_profiler verwenden. Ich habe ein bisschen damit gespielt und bin mir nicht sicher, ob ich den Ergebnissen vertraue. Die Verwendung ist sehr einfach. Sie dekorieren eine Funktion (dies könnte die Hauptfunktion (0-Funktion) mit @profiler decorator sein, und wenn das Programm beendet wird, druckt der Memory Profiler einen praktischen Bericht, der die Gesamtzahl und die Änderungen im Speicher für jede Zeile anzeigt. Hier ein Beispiel Programm, das ich unter dem Profiler lief:

msgstr "Python vom Speicherprofilprofil importieren

@profile def main (): a = [] b = [] c = [] für i im Bereich (100000): a.append (5) für i im Bereich (100000): b.append (300) für i in Bereich (100000): c.append ('123456789012345678901234567890') del a del b del c

print "Fertig!" wenn __name__ == '__main__': main () "

Hier ist die Ausgabe:

Zeile # Mem Verwendung Inkrementieren Zeileninhalt ============================================ ===== 3 22,9 MiB 0,0 MiB @Profil 4 def main (): 5 22,9 MiB 0,0 MiB a = [] 6 22,9 MiB 0,0 MiB b = [] 7 22,9 MiB 0,0 MiB c = [] 8 27,1 MiB 4,2 MiB für i in Reichweite (100000): 9 27,1 MiB 0,0 MiB a.append (5) 10 27,5 MiB 0,4 MiB für i in Reichweite (100000): 11 27,5 MiB 0,0 MiB b.append (300) 12 28,3 MiB 0,8 ​​MiB für i im Bereich (100000): 13 28,3 MiB 0,0 MiB c. Append ('123456789012345678901234567890') 14 27,7 MiB -0,6 MiB del a 15 27,9 MiB 0,2 MiB del b 16 27,3 MiB del c 17 18 27,3 MiB 0,0 MiB drucken ' Erledigt!' 

Wie Sie sehen, gibt es 22,9 MB Arbeitsspeicher. Der Grund dafür, dass der Speicher beim Hinzufügen von Ganzzahlen innerhalb und außerhalb des Bereichs [-5, 256] nicht zunimmt, und auch beim Hinzufügen der Zeichenfolge ist, dass in allen Fällen ein einzelnes Objekt verwendet wird. Es ist nicht klar, warum die erste Schleife des Bereichs (100000) in Zeile 8 4,2 MB hinzufügt, während die zweite in Zeile 10 nur 0,4 MB und die dritte Schleife in Zeile 12 0,8 MB hinzufügt. Schließlich werden beim Löschen der a-, b- und c-Listen -0,6 MB für a und c freigegeben, aber für b werden 0,2 MB hinzugefügt. Ich kann aus diesen Ergebnissen nicht viel Sinn machen.

Fazit

CPython verwendet für seine Objekte viel Speicher. Es verwendet verschiedene Tricks und Optimierungen für die Speicherverwaltung. Indem Sie die Speicherauslastung Ihres Objekts nachverfolgen und sich des Speicherverwaltungsmodells bewusst sind, können Sie den Speicherbedarf Ihres Programms erheblich reduzieren.

Lerne Python

Lernen Sie Python mit unserem kompletten Python-Tutorial, egal ob Sie gerade erst anfangen oder ein erfahrener Programmierer sind, der neue Fähigkeiten erlernen möchte.