Einige Gedanken zu diesem Artikel .Kürzlich interessierte mich, wie Python Memory Management in CPython für Python3
für 64-Bit-Ubuntu funktioniert.
Ein bisschen Theorie
Die glibc-Systembibliothek verfügt über einen Malloc-Allokator. Jeder Prozess verfügt über einen Speicherbereich, der als Heap bezeichnet wird. Durch die dynamische Zuweisung von Speicher durch Aufrufen der malloc-Funktion erhalten wir einen Teil aus dem Haufen dieses Prozesses. Wenn die Größe des angeforderten Speichers klein ist (nicht mehr als 128 KB), kann der Speicher aus den Listen der freien Blöcke entnommen werden. Ist dies nicht möglich, wird der Speicher mit dem
Systemaufruf mmap
(sbrk, brk) zugewiesen . Der Systemaufruf mmap ordnet den virtuellen Speicher dem physischen Speicher zu. Der Speicher wird in 4-KB-Seiten angezeigt. Große Chunks (über 128 KB) werden immer über den mmap-Systemaufruf zugewiesen. Wenn beim Freigeben von Speicher ein freier kleiner Block an einen Bereich mit nicht eingefrorenem Speicher angrenzt, wird möglicherweise ein Teil des Speichers an das Betriebssystem zurückgegeben. Große Chunks kehren sofort zum Betriebssystem zurück.
Informationen aus einem
Vortrag über Allokatoren in C.CPython hat einen eigenen Allokator (PyMalloc) für den "privaten Heap" und Allokatoren für jeden Objekttyp, der "über" dem ersten arbeitet. PyMalloc fordert 256-KB-Speicherabschnitte über malloc in der Betriebssystembibliothek namens Arenas an. Sie sind wiederum durch 4 KB in Pools unterteilt. Jeder Pool ist in Blöcke fester Größe unterteilt, und jeder kann in Blöcke einer von 64 Größen unterteilt werden.
Allokatoren für jeden Typ verwenden die bereits zugewiesenen Chunks, falls vorhanden. Wenn keine vorhanden sind, gibt PyMalloc einen neuen Pool aus der ersten Arena aus, in dem ein Platz für einen neuen Pool vorhanden ist (Arenen werden in absteigender Reihenfolge der Belegung „sortiert“). Wenn dies nicht funktioniert, fragt PyMalloc das Betriebssystem nach einer neuen Arena. Außer wenn die angeforderte Speichergröße mehr als 512B beträgt, wird der Speicher direkt über malloc aus der Systembibliothek zugewiesen.
Wenn ein Objekt gelöscht wird, wird der Speicher nicht an das Betriebssystem zurückgegeben, sondern die Chunks kehren einfach zu den entsprechenden Pools und die Pools zu den Arenen zurück. Die Arena kehrt zum Betriebssystem zurück, wenn alle Chunks davon befreit sind. Wie sich herausstellt, wird der gesamte Speicher in der Arena von PVM verwendet, wenn eine relativ kleine Anzahl von Chunks in der Arena verwendet wird. Da Chunks über 128 KB über mmap zugewiesen werden, kehrt die kostenlose Arena sofort zum Betriebssystem zurück.
Ich möchte mich auf zwei Punkte konzentrieren:
- Es stellt sich heraus, dass PyMalloc beim Erstellen einer neuen Arena 256 KB physischen Speicher zuweist.
- Es werden nur freie Arenen an das Betriebssystem zurückgegeben.
Beispiel
Betrachten Sie das folgende Beispiel:
iterations = 2000000 l = [] for i in range(iterations): l.append(None) for i in range(iterations): l[i] = {} s = []
Im Beispiel wird eine Liste mit 2 Millionen Elementen erstellt, die alle auf ein Objekt mit dem Namen None verweisen. Im nächsten Zyklus wird für jedes Element ein Objekt erstellt - ein leeres Wörterbuch. Dann wird eine zweite Liste erstellt, deren Elemente auf einige Objekte verweisen, auf die von einigen Elementen der ersten Liste verwiesen wird. Nach dem nächsten Crawl beginnen die Elemente aus der Liste l wieder auf das Objekt None zu zeigen. Und im letzten Zyklus werden wieder Wörterbücher für jedes Element aus der ersten Liste erstellt.
Optionen der S-Liste:
s = []
s = l[::2]
s = l[200000 // 2::]
s = l[::100]
Wir sind jeweils am Speicherverbrauch interessiert.
Wir werden dieses Skript mit aktivierter PyMalloc-Protokollierung ausführen:
export PYTHONMALLOCSTATS="True" && python3 source.py 2>result.txt
Erläuterung der Ergebnisse
In den Bildern sehen Sie jeweils den Speicherverbrauch.
Auf der Abszissenachse gibt es keine Korrelation von Werten mit dem Zeitpunkt, zu dem ein solcher Verbrauch stattfand. Nur jeder Wert in den Protokollen ist mit seiner Seriennummer verknüpft.
"Ohne Elemente"
Im ersten Fall ist die Liste leer. Nach dem Erstellen der Objekte im zweiten Zyklus werden ca. 500 MB Speicher verbraucht. Alle diese Objekte werden im dritten Zyklus gelöscht, und der verwendete Speicher wird an das Betriebssystem zurückgegeben. Im letzten Zyklus wird erneut Speicher für Objekte zugewiesen, was zu einem Verbrauch von 500 MB führt.
"Jede Sekunde"
In dem Fall, dass wir mit jedem zweiten Element der Liste 1 eine Liste erstellen, können wir feststellen, dass der Speicher nicht an das Betriebssystem zurückgegeben wird. Das heißt, in diesem Fall stellen wir eine Situation fest, in der Wörterbücher mit ungefähr 250 MB gelöscht werden, in jedem Pool jedoch Chunks vorhanden sind, die nicht gelöscht werden, wodurch die entsprechenden Arenen nicht freigegeben werden. Wenn wir die Wörterbücher jedoch zum zweiten Mal erstellen, werden die freien Chunks aus diesen Pools wiederverwendet, weshalb nur etwa 250 MB neuer Speicher zugewiesen werden.
"Die zweite Hälfte"
In dem Fall, dass wir eine Liste aus der zweiten Hälfte der Elemente der Liste 1 erstellen, befindet sich die erste Hälfte in separaten Arenen, wodurch ungefähr 250 MB Speicher an das Betriebssystem zurückgegeben werden. Danach werden ca. 500 MB neuen Wörterbüchern zugewiesen, weshalb der Gesamtverbrauch in der Region von 750 MB liegt.
In diesem Fall wird der Speicher, anders als im zweiten Fall, teilweise an das Betriebssystem zurückgegeben. Was einerseits anderen Prozessen ermöglicht, diesen Speicher zu nutzen, erfordert andererseits Systemaufrufe, um ihn freizugeben und neu zuzuweisen.
"Jeder Hundertste"
Der letzte Fall scheint der interessanteste zu sein. Dort erstellen wir aus jedem hundertsten Element der ersten Liste eine zweite Liste, die ungefähr 5 MB benötigt. Aufgrund der Tatsache, dass eine bestimmte Anzahl von belegten Chunks in jeder Arena verbleibt, wird dieser Speicher nicht freigegeben und der Verbrauch bleibt auf dem Niveau von 500 MB. Wenn Sie Wörterbücher zum zweiten Mal erstellen, wird fast kein neuer Speicher zugewiesen, und die Chunks, die zum ersten Mal zugewiesen wurden, werden wiederverwendet.
In dieser Situation verbrauchen wir aufgrund der Speicherfragmentierung 100-mal mehr, als wir benötigen. Wenn dieser Speicher jedoch wiederholt benötigt wird, müssen wir keine Systemaufrufe durchführen, um ihn zuzuweisen.
Zusammenfassung
Es ist anzumerken, dass eine Speicherfragmentierung möglich ist, wenn viele Allokatoren verwendet werden. Trotzdem müssen Sie einige Datenstrukturen, z. B. solche mit Baumstruktur, wie Suchbäume, sorgfältig verwenden. Weil willkürliche Operationen des Hinzufügens und Löschens zu der oben beschriebenen Situation führen können, weshalb die Zweckmäßigkeit der Verwendung dieser Strukturen hinsichtlich des Speicherverbrauchs zweifelhaft sein wird.
Code zum Rendern von Bildern def parse_result(filename): ms = [] with open(filename, "r") as f: for line in f: if line.startswith("Total"): m = float(line.split()[-1].replace(",", "")) / 1024 / 1024 ms.append(m) return ms ms_1 = parse_result("_1.txt") ms_2 = parse_result("_2.txt") ms_3 = parse_result("_3.txt") ms_4 = parse_result("_4.txt") import matplotlib.pyplot as plt plt.figure(figsize=(20, 15)) fontdict = { "fontsize": 20, "fontweight" : 1, } plt.subplot(2, 2, 1) plt.title(" ", fontdict=fontdict, loc="left") plt.plot(ms_1) plt.grid(b=True, which='major', color='#666666', linestyle='-.') plt.minorticks_on() plt.grid(b=True, which='minor', color='#999999', linestyle='-', alpha=0.2) plt.tick_params(axis='both', which='major', labelsize=15, labelbottom=False) plt.ylabel("MB", fontsize=15) plt.subplot(2, 2, 2) plt.title(" ", fontdict=fontdict, loc="left") plt.plot(ms_2) plt.grid(b=True, which='major', color='#666666', linestyle='-.') plt.minorticks_on() plt.grid(b=True, which='minor', color='#999999', linestyle='-', alpha=0.2) plt.tick_params(axis='both', which='major', labelsize=15, labelbottom=False) plt.ylabel("MB", fontsize=15) plt.subplot(2, 2, 3) plt.title(" ", fontdict=fontdict, loc="left") plt.plot(ms_3) plt.grid(b=True, which='major', color='#666666', linestyle='-.') plt.minorticks_on() plt.grid(b=True, which='minor', color='#999999', linestyle='-', alpha=0.2) plt.tick_params(axis='both', which='major', labelsize=15, labelbottom=False) plt.ylabel("MB", fontsize=15) plt.subplot(2, 2, 4) plt.title(" ", fontdict=fontdict, loc="left") plt.plot(ms_4) plt.grid(b=True, which='major', color='#666666', linestyle='-.') plt.minorticks_on() plt.grid(b=True, which='minor', color='#999999', linestyle='-', alpha=0.2) plt.tick_params(axis='both', which='major', labelsize=15, labelbottom=False) plt.ylabel("MB", fontsize=15)