2.40. Speicher und Garbage Collection¶
MicroPython verwaltet Speicher auf dieselbe Weise wie CPython: Jedes Objekt lebt auf dem Heap, und ein Garbage Collector (GC) gibt Objekte frei, die von nichts mehr referenziert werden. Auf einem Gerät mit einigen hundert Kilobyte RAM ist es gelegentlich notwendig, darauf zu achten, wie der Heap genutzt wird; auf Desktop-Python ist das selten der Fall.
2.40.1. Der Heap¶
Der Heap ist der Bereich des RAM – auf den meisten OpenMV-Kameras tatsächlich über mehr als einen physischen Speicherpool aufgeteilt –, den die Laufzeitumgebung an Python-Objekte vergibt. Jedes Mal, wenn ein Python-Ausdruck ein neues Objekt erzeugt (eine Liste, eine Zeichenkette, ein dict, ein Tupel, alles, was keine kleine Ganzzahl oder ein Singleton ist), wird ein Block von Bytes aus dem Heap entnommen, um es aufzunehmen. Wenn der GC bemerkt, dass ein Objekt nicht mehr erreichbar ist, gibt er den Block an den freien Pool zurück, aus dem er stammte.
Zwei Funktionen im Modul gc sind kennenswert:
gc.mem_free()– ungefähre Anzahl der momentan freien Bytes auf dem Heap.gc.collect()– führt sofort einen Sammelzyklus aus, anstatt zu warten, bis die Laufzeitumgebung einen auslöst.
import gc
print("before:", gc.mem_free())
big = [0] * 10000
print("after :", gc.mem_free())
del big
gc.collect()
print("freed :", gc.mem_free())
Die genauen Zahlen hängen vom Build ab; entscheidend ist die Richtung der Änderung: Das Allozieren großer Dinge verringert mem_free, und das Verwerfen der Referenzen plus ein gc.collect gibt den Heap zurück.
2.40.2. Fragmentierung¶
Der freie Pool ist nicht wie von Zauberhand zusammenhängend. Da Objekte mit unterschiedlichen Lebensdauern kommen und gehen, zerfällt der freie Speicher in immer kleinere Stücke, selbst wenn seine Gesamtgröße noch groß ist. Das Allozieren eines Objekts, das größer ist als das größte einzelne freie Stück, schlägt mit MemoryError fehl – obwohl technisch genug freier RAM insgesamt vorhanden ist.
Derselbe gesamte freie RAM kann einen großen Puffer aufnehmen (links) oder dies verweigern (rechts), je nachdem, wie fragmentiert er ist.¶
Fragmentierung ist meist nur bei lang laufenden Skripten ein Problem, die fortlaufend unterschiedlich große Puffer allozieren und freigeben. Die mit Abstand wirksamste Gegenmaßnahme besteht darin, langlebige Puffer nahe dem Programmanfang vorab zu allozieren, bevor viele kurzlebige Allozierungen die Gelegenheit hatten, sie zu verstreuen.
2.40.3. Vorab-Allozierung¶
Zwei Muster sorgen dafür, dass sich der Heap gutartig verhält:
Allozieren Sie Puffer fester Größe einmal und verwenden Sie sie wieder, anstatt pro Iteration eine neue Liste oder ein neues
bytearrayaufzubauen.Ziehen Sie Konstanten und Nachschlagetabellen aus inneren Schleifen heraus, sodass sie einmal erzeugt werden.
buf = bytearray(64) # one allocation, reused below
def fill(value):
for i in range(len(buf)):
buf[i] = value
fill(0)
fill(255)
Vergleichen Sie dies mit einer Version, die innerhalb der Schleife ein neues bytearray erzeugt: Jede Iteration produziert Müll, den der GC später aufräumen muss. Die vorab allozierte Version erzeugt überhaupt keinen Müll.
2.40.4. Wann gc.collect aufzurufen ist¶
gc.collect() läuft normalerweise automatisch – die Laufzeitumgebung löst es aus, wenn Allozierungen nicht genug freien Speicher finden. Es von Hand aufzurufen ist in zwei Situationen nützlich:
Direkt nachdem ein großer Stapel von Objekten den Gültigkeitsbereich verlassen hat, um sie sofort freizugeben, anstatt zu warten, bis die nächste Allozierung den Preis dafür zahlt.
Direkt vor einem Abschnitt, der eine bekannte maximale Menge an freiem Speicher benötigt, um zu vermeiden, dass der GC mitten in einer zeitkritischen Operation anspringt.
gc.collect-Aufrufe überall zu verstreuen macht ein Programm nicht schneller – die Sammlung selbst kostet Zeit. Verwenden Sie es gezielt, an Stellen, an denen die Kosten einer ungeplanten Sammlung schlimmer wären als die Kosten einer bewusst ausgeführten.