Speicher in Python freigeben

128

Ich habe im folgenden Beispiel einige verwandte Fragen zur Speichernutzung.

  1. Wenn ich im Dolmetscher laufe,

    foo = ['bar' for _ in xrange(10000000)]

    Der auf meinem Computer verwendete reale Speicher reicht bis zu 80.9mb. Ich habe dann,

    del foo

    echte Erinnerung geht runter, aber nur zu 30.4mb. Der Interpreter verwendet die 4.4mbBaseline. Was ist also der Vorteil, wenn kein 26mbSpeicher für das Betriebssystem freigegeben wird ? Liegt es daran, dass Python "im Voraus plant" und denkt, dass Sie möglicherweise wieder so viel Speicher verwenden?

  2. Warum wird es 50.5mbspeziell veröffentlicht - auf welcher Höhe wird es veröffentlicht?

  3. Gibt es eine Möglichkeit, Python zu zwingen, den gesamten verwendeten Speicher freizugeben (wenn Sie wissen, dass Sie nicht mehr so ​​viel Speicher verwenden werden)?

HINWEIS Diese Frage unterscheidet sich von Wie kann ich explizit Speicher in Python freigeben? denn diese Frage befasst sich hauptsächlich mit der Erhöhung der Speichernutzung gegenüber dem Ausgangswert, selbst nachdem der Interpreter Objekte über die Speicherbereinigung freigegeben hat (mit gc.collectoder ohne Verwendung).

Jared
quelle
4
Es ist erwähnenswert, dass dieses Verhalten nicht spezifisch für Python ist. Im Allgemeinen wird der Speicher erst dann an das Betriebssystem zurückgegeben, wenn ein Prozess einen vom Heap zugewiesenen Speicher freigibt, bis der Prozess beendet ist.
NPE
Ihre Frage stellt mehrere Dinge - von denen einige Dups sind, von denen einige für SO ungeeignet sind, von denen einige gute Fragen sein können. Fragen Sie sich, ob Python keinen Speicher freigibt, unter welchen Umständen es kann / nicht kann, was der zugrunde liegende Mechanismus ist, warum es so entworfen wurde, ob es Problemumgehungen gibt oder etwas ganz anderes?
abarnert
2
@abarnert Ich habe ähnliche Unterfragen kombiniert. Um auf Ihre Fragen zu antworten: Ich weiß, dass Python etwas Speicher für das Betriebssystem freigibt, aber warum nicht alles und warum die Menge, die es tut. Wenn es Umstände gibt, unter denen dies nicht möglich ist, warum? Welche Problemumgehungen auch.
Jared
@jww Das glaube ich nicht. Diese Frage bezog sich wirklich darauf, warum der Interpreter-Prozess selbst nach dem vollständigen Sammeln von Müll mit Aufrufen von nie Speicher freigegeben hat gc.collect.
Jared

Antworten:

86

Der auf dem Heap zugewiesene Speicher kann Hochwassermarken unterliegen. Dies wird durch die internen Optimierungen von Python für die Zuweisung kleiner Objekte ( PyObject_Malloc) in 4 KiB-Pools erschwert , die für Zuordnungsgrößen bei Vielfachen von 8 Bytes klassifiziert sind - bis zu 256 Bytes (512 Bytes in 3.3). Die Pools selbst befinden sich in 256-KiB-Arenen. Wenn also nur ein Block in einem Pool verwendet wird, wird die gesamte 256-KiB-Arena nicht freigegeben. In Python 3.3 wurde der Allokator für kleine Objekte auf die Verwendung anonymer Speicherzuordnungen anstelle des Heapspeichers umgestellt, sodass die Speicherfreigabe besser sein sollte.

Darüber hinaus verwalten die integrierten Typen Freelists von zuvor zugewiesenen Objekten, die möglicherweise den Allokator für kleine Objekte verwenden oder nicht. Der intTyp verwaltet eine freie Liste mit seinem eigenen zugewiesenen Speicher, und das Löschen erfordert einen Aufruf PyInt_ClearFreeList(). Dies kann indirekt durch eine vollständige aufgerufen werden gc.collect.

Versuchen Sie es so und sagen Sie mir, was Sie bekommen. Hier ist der Link für psutil.Process.memory_info .

import os
import gc
import psutil

proc = psutil.Process(os.getpid())
gc.collect()
mem0 = proc.get_memory_info().rss

# create approx. 10**7 int objects and pointers
foo = ['abc' for x in range(10**7)]
mem1 = proc.get_memory_info().rss

# unreference, including x == 9999999
del foo, x
mem2 = proc.get_memory_info().rss

# collect() calls PyInt_ClearFreeList()
# or use ctypes: pythonapi.PyInt_ClearFreeList()
gc.collect()
mem3 = proc.get_memory_info().rss

pd = lambda x2, x1: 100.0 * (x2 - x1) / mem0
print "Allocation: %0.2f%%" % pd(mem1, mem0)
print "Unreference: %0.2f%%" % pd(mem2, mem1)
print "Collect: %0.2f%%" % pd(mem3, mem2)
print "Overall: %0.2f%%" % pd(mem3, mem0)

Ausgabe:

Allocation: 3034.36%
Unreference: -752.39%
Collect: -2279.74%
Overall: 2.23%

Bearbeiten:

Ich habe auf die Messung relativ zur Prozess-VM-Größe umgestellt, um die Auswirkungen anderer Prozesse im System zu eliminieren.

Die C-Laufzeit (z. B. glibc, msvcrt) verkleinert den Heap, wenn der zusammenhängende freie Speicherplatz oben einen konstanten, dynamischen oder konfigurierbaren Schwellenwert erreicht. Mit glibc können Sie dies mit mallopt(M_TRIM_THRESHOLD) einstellen. Angesichts dessen ist es nicht verwunderlich, wenn der Haufen um mehr - sogar viel mehr - schrumpft als der Block, den Sie haben free.

In 3.x rangewird keine Liste erstellt, sodass im obigen Test keine 10 Millionen intObjekte erstellt werden. Selbst wenn dies der Fall intist , ist der Typ in 3.x im Grunde genommen ein 2.x long, der keine Freelist implementiert.

Eryk Sun.
quelle
Verwendung memory_info()anstelle von get_memory_info()und xist definiert
Aziz Alto
Sie erhalten intsogar in Python 3 10 ^ 7 s, aber jede ersetzt die letzte in der Schleifenvariablen, sodass nicht alle gleichzeitig existieren .
Davis Herring
Ich habe ein Problem mit Speicherverlusten festgestellt, und ich denke, der Grund dafür ist, was Sie hier beantwortet haben. Aber wie kann ich meine Vermutung beweisen? Gibt es ein Tool, das anzeigen kann, dass viele Pools malloced sind, aber nur ein kleiner Block verwendet wird?
ruiruige1991
130

Ich vermute, die Frage, die Sie hier wirklich interessiert, ist:

Gibt es eine Möglichkeit, Python zu zwingen, den gesamten verwendeten Speicher freizugeben (wenn Sie wissen, dass Sie nicht mehr so ​​viel Speicher verwenden werden)?

Nein, da ist kein. Es gibt jedoch eine einfache Problemumgehung: untergeordnete Prozesse.

Wenn Sie 5 Minuten lang 500 MB temporären Speicher benötigen, danach aber noch 2 Stunden laufen müssen und nie wieder so viel Speicher berühren, erstellen Sie einen untergeordneten Prozess, um die speicherintensive Arbeit auszuführen. Wenn der untergeordnete Prozess beendet wird, wird der Speicher freigegeben.

Dies ist nicht ganz trivial und kostenlos, aber es ist ziemlich einfach und billig, was normalerweise gut genug ist, damit sich der Handel lohnt.

Erstens ist der einfachste Weg, einen untergeordneten Prozess zu erstellen, mit concurrent.futures(oder für 3.1 und früher den futuresBackport auf PyPI):

with concurrent.futures.ProcessPoolExecutor(max_workers=1) as executor:
    result = executor.submit(func, *args, **kwargs).result()

Wenn Sie etwas mehr Kontrolle benötigen, verwenden Sie das multiprocessingModul.

Die Kosten betragen:

  • Der Prozessstart ist auf einigen Plattformen, insbesondere unter Windows, ziemlich langsam. Wir sprechen hier von Millisekunden, nicht von Minuten, und wenn Sie ein Kind für 300 Sekunden arbeiten lassen, werden Sie es nicht einmal bemerken. Aber es ist nicht kostenlos.
  • Wenn die große Menge an temporärem Speicher, die Sie verwenden, wirklich groß ist , kann dies dazu führen, dass Ihr Hauptprogramm ausgelagert wird. Natürlich sparen Sie auf lange Sicht Zeit, denn wenn dieser Speicher für immer hängen bleibt, muss er irgendwann zum Austausch führen. Dies kann jedoch in einigen Anwendungsfällen zu einer allmählichen Langsamkeit in sehr merklichen (und frühen) Verzögerungen führen.
  • Das Senden großer Datenmengen zwischen Prozessen kann langsam sein. Wenn Sie über das Senden von mehr als 2.000 Argumenten und das Zurückholen von 64.000 Ergebnissen sprechen, werden Sie dies nicht einmal bemerken. Wenn Sie jedoch große Datenmengen senden und empfangen, sollten Sie einen anderen Mechanismus verwenden (eine Datei, mmapped oder anderweitig; die Shared-Memory-APIs in multiprocessing; usw.).
  • Das Senden großer Datenmengen zwischen Prozessen bedeutet, dass die Daten auswählbar sein müssen (oder, wenn Sie sie in eine Datei oder einen gemeinsam genutzten Speicher structstecken , -able oder idealerweise ctypes-able).
abarnert
quelle
Wirklich schöner Trick, obwohl er das Problem nicht löst :( Aber ich mag es wirklich
ddofborg
32

Eryksun hat Frage 1 beantwortet, und ich habe Frage 3 (das Original Nr. 4) beantwortet, aber jetzt beantworten wir Frage 2:

Warum werden insbesondere 50,5 MB veröffentlicht - auf welcher Menge wird veröffentlicht?

Worauf es basiert, ist letztendlich eine ganze Reihe von Zufällen in Python malloc, die sehr schwer vorherzusagen sind.

Erstens, je nachdem, wie Sie den Speicher messen, messen Sie möglicherweise nur Seiten, die tatsächlich dem Speicher zugeordnet sind. In diesem Fall wird der Speicher jedes Mal, wenn eine Seite vom Pager ausgetauscht wird, als "freigegeben" angezeigt, obwohl er nicht freigegeben wurde.

Oder Sie messen verwendete Seiten, die zugewiesene, aber nie berührte Seiten zählen (auf Systemen, die optimistisch zu viel zuweisen, wie z. B. Linux), zugewiesene, aber gekennzeichnete Seiten MADV_FREEusw.

Wenn Sie wirklich zugewiesene Seiten messen (was eigentlich nicht sehr nützlich ist, aber es scheint das zu sein, worüber Sie fragen) und Seiten wirklich freigegeben wurden, zwei Umstände, unter denen dies passieren kann: Entweder Sie ' Sie haben brkoder äquivalent verwendet, um das Datensegment zu verkleinern (heutzutage sehr selten), oder Sie haben munmapoder ähnlich verwendet, um ein zugeordnetes Segment freizugeben. (Theoretisch gibt es auch eine kleinere Variante zu letzterem, da es Möglichkeiten gibt, einen Teil eines zugeordneten Segments freizugeben - z. B. ihn MAP_FIXEDfür ein MADV_FREESegment zu stehlen, das Sie sofort entfernen).

Aber die meisten Programme ordnen Dinge nicht direkt aus Speicherseiten zu; Sie verwenden einen mallocAllokator. Wenn Sie aufrufen free, kann der Allokator Seiten nur dann an das Betriebssystem freigeben, wenn Sie gerade freedas letzte Live-Objekt in einem Mapping (oder auf den letzten N Seiten des Datensegments) sind. Ihre Anwendung kann dies auf keinen Fall vernünftigerweise vorhersagen oder sogar im Voraus erkennen, dass dies geschehen ist.

CPython macht dies noch komplizierter: Es verfügt über einen benutzerdefinierten 2-Ebenen-Objektzuweiser über einem benutzerdefinierten Speicherzuweiser malloc. (Siehe die Quelle Kommentar für eine ausführlichere Erklärung.) Und oben auf , dass auch bei der C - API - Ebene, viel weniger Python, Sie haben nicht einmal direkt steuern , wenn die Top-Level - Objekte freigegeben werden.

Wenn Sie also ein Objekt freigeben, woher wissen Sie, ob es Speicher für das Betriebssystem freigibt? Nun, zuerst müssen Sie wissen, dass Sie die letzte Referenz veröffentlicht haben (einschließlich aller internen Referenzen, von denen Sie nichts wussten), damit der GC sie freigeben kann. (Im Gegensatz zu anderen Implementierungen wird mindestens CPython die Zuordnung eines Objekts aufheben, sobald dies zulässig ist.) Dadurch werden normalerweise mindestens zwei Dinge auf der nächsten Ebene freigegeben (z. B. geben Sie für eine Zeichenfolge das PyStringObjekt und den Zeichenfolgenpuffer frei ).

Wenn Sie die Zuordnung eines Objekts aufheben, müssen Sie den internen Status des Objektzuordners sowie dessen Implementierung kennen, um zu wissen, ob die nächste Ebene die Freigabe eines Objektspeicherblocks aufhebt. (Es kann offensichtlich nur passieren, wenn Sie das letzte Element im Block freigeben, und selbst dann kann es nicht passieren.)

Wenn Sie die Zuordnung eines Objektspeicherblocks freeaufheben, müssen Sie den internen Status des PyMem-Allokators sowie dessen Implementierung kennen, um festzustellen, ob dies einen Aufruf verursacht . (Auch hier müssen Sie die Zuordnung des letzten verwendeten Blocks innerhalb einer malloced-Region aufheben , und selbst dann kann dies möglicherweise nicht passieren.)

Wenn Sie tun free eine malloced Region, wissen , ob dies ein verursacht munmapoder gleichwertig (oder brk), müssen Sie den internen Zustand der weiß malloc, und wie es umgesetzt wird . Und dieser ist im Gegensatz zu den anderen sehr plattformspezifisch. (Und wieder müssen Sie im Allgemeinen die Zuordnung der zuletzt verwendeten mallocinnerhalb eines mmapSegments freigeben, und selbst dann kann dies möglicherweise nicht passieren.)

Wenn Sie also verstehen möchten, warum genau 50,5 MB veröffentlicht wurden, müssen Sie dies von unten nach oben verfolgen. Warum wurde die mallocZuordnung von Seiten im Wert von 50,5 MB aufgehoben, wenn Sie einen oder mehrere freeAufrufe getätigt haben (wahrscheinlich etwas mehr als 50,5 MB)? Sie müssten die Ihrer Plattform lesen mallocund dann die verschiedenen Tabellen und Listen durchsuchen, um den aktuellen Status anzuzeigen. (Auf einigen Plattformen werden möglicherweise sogar Informationen auf Systemebene verwendet, die so gut wie unmöglich zu erfassen sind, ohne einen Schnappschuss des Systems zu erstellen, um sie offline zu überprüfen. Glücklicherweise ist dies jedoch normalerweise kein Problem.) Und dann müssen Sie Machen Sie dasselbe auf den 3 darüber liegenden Ebenen.

Die einzig nützliche Antwort auf die Frage lautet "Weil".

Sofern Sie keine ressourcenbeschränkte (z. B. eingebettete) Entwicklung durchführen, haben Sie keinen Grund, sich um diese Details zu kümmern.

Und wenn Sie eine ressourcenbeschränkte Entwicklung durchführen, ist es nutzlos, diese Details zu kennen. Sie müssen so ziemlich alle diese Ebenen und insbesondere mmapden Speicher, den Sie auf Anwendungsebene benötigen, beenden (möglicherweise mit einem einfachen, gut verstandenen, anwendungsspezifischen Zonenzuweiser dazwischen).

abarnert
quelle
2

Zunächst möchten Sie möglicherweise Blicke installieren:

sudo apt-get install python-pip build-essential python-dev lm-sensors 
sudo pip install psutil logutils bottle batinfo https://bitbucket.org/gleb_zhulik/py3sensors/get/tip.tar.gz zeroconf netifaces pymdstat influxdb elasticsearch potsdb statsd pystache docker-py pysnmp pika py-cpuinfo bernhard
sudo pip install glances

Dann führen Sie es im Terminal!

glances

Fügen Sie in Ihrem Python-Code am Anfang der Datei Folgendes hinzu:

import os
import gc # Garbage Collector

Nachdem Sie die Variable "Big" (zum Beispiel: myBigVar) verwendet haben, für die Sie Speicher freigeben möchten, schreiben Sie Folgendes in Ihren Python-Code:

del myBigVar
gc.collect()

Führen Sie in einem anderen Terminal Ihren Python-Code aus und beobachten Sie im "Blick" -Terminal, wie der Speicher in Ihrem System verwaltet wird!

Viel Glück!

PS Ich nehme an, Sie arbeiten an einem Debian- oder Ubuntu-System

de20ce
quelle