4.19. Memory-Pools¶
Eine Kamera, die drei Einzelbilder in voller Auflösung in einem Framebuffer-Pool hält, daneben einen separaten Vorschaupuffer betreibt und außerdem noch Platz für ein Python-Skript und dessen Objekte bietet, jongliert mit mehr Speicher, als ein einzelner RAM-Block des MCU bereitstellen könnte. MicroPython bringt alles unter, indem es es auf die mehreren verschiedenen Speicherarten verteilt, die der MCU bereitstellt, und indem es jede Art von Allokation an die Speicherart leitet, die sie tatsächlich benötigt.
4.19.1. Speicherarten¶
Ein moderner OpenMV Cam MCU stellt vier verschiedene Speicherarten bereit. Die erste ist für die Anwendung unsichtbar; die anderen drei sind Pools, aus denen Allokationen stammen können.
Der Daten-Cache der CPU – ein kleiner, sehr schneller Speicherbereich, der zwischen der CPU und dem restlichen RAM sitzt. Wenn die CPU einen Wert aus dem Hauptspeicher liest oder schreibt, behält der Cache automatisch eine Kopie, sodass wiederholte Zugriffe auf dieselben Daten im Cache bleiben und nie die Kosten für den Weg zum langsameren Speicher zahlen. Der Cache ist kein Pool, aus dem Allokationen stammen. Er ist für die Anwendung transparent – er lässt den restlichen RAM in der Praxis einfach schneller wirken, als seine reine Latenz vermuten ließe, bis zu dem Punkt, an dem ein Arbeitssatz nicht mehr hineinpasst.
Eng an den Prozessor gekoppelter Speicher – ein kleiner RAM-Block, der ohne dazwischenliegenden Bus direkt mit der CPU verdrahtet ist. Zugriff in einem einzigen Takt, nie ein Fehlzugriff, nie eine Wartezeit. Allokationen, die wirklich den schnellstmöglichen Speicher benötigen – bei denen jeder Takt an Latenz zählt – stammen aus diesem Pool.
Schneller On-Chip-Speicher – ein paar hundert Kilobyte bis etwa ein Megabyte RAM, das in das MCU-Gehäuse integriert ist. Geringe Latenz, hohe Bandbreite, aber begrenzte Größe. Der MicroPython-Heap liegt hier, damit Zugriffe auf Python-Objekte schnell bleiben; kleinere Arbeitspuffer, die die CPU häufig anspricht, teilen sich den Pool.
Langsamerer Massenspeicher – auf Boards, die den MCU mit einem externen Speicherchip kombinieren, mehrere zehn Megabyte externer RAM, der über den externen Bus erreicht wird. Deutlich größer, aber jeder Zugriff dauert länger als bei On-Chip-Speicher; der Daten-Cache verbirgt einen Großteil dieser Kosten für Arbeitssätze, die er aufnehmen kann, und der Unterschied zeigt sich bei Operationen, die über Daten streichen, die zu groß zum Cachen sind. Wird für Allokationen verwendet, die groß sein müssen und bei denen die CPU eine geringere Geschwindigkeit tolerieren kann – am wichtigsten der Framebuffer-Pool.
Boards der Familie liegen auf einem Spektrum: Manche haben nur On-Chip-RAM; manche kombinieren On-Chip-RAM mit einem viel größeren externen Block. Jede der drei allokierbaren Arten wird als Memory-Pool behandelt – ein Block, aus dem Allokationen stammen – und so gekennzeichnet, dass jede Anfrage die Speicherart anfordern kann, die sie tatsächlich benötigt.
4.19.2. Der primäre Framebuffer¶
Der Framebuffer, der snapshot() zugrunde liegt, fordert keinen schnellen Speicher an. Er fordert genug Speicher an – nicht mehr. Das platziert ihn in dem jeweils größten Pool, sodass der Framebuffer auf einem Board mit sowohl On-Chip- als auch externem Speicher im externen Block landet.
Ein dreifach gepufferter Framebuffer in voller Auflösung ist auf den meisten Bausteinen viel zu groß, um in den schnellen On-Chip-Pool zu passen; der größere Pool ist der einzige, der ihn überhaupt aufnehmen kann. Der Daten-Cache der CPU verbirgt einen Großteil der Kosten pro Zugriff, wenn die Anwendung das Bild verarbeitet, und die DMA-Engine, die den Framebuffer aus dem Sensor füllt, hält in beiden Fällen mit der Datenrate des Sensors Schritt.
Die genaue Größe, die der Framebuffer einnimmt, wird aus dem aktuellen pixformat(), dem framesize() und der framebuffers()-Anzahl ermittelt; sie wächst oder schrumpft jedes Mal, wenn sich eines davon ändert.
4.19.3. Sekundäre Sensor-Framebuffer¶
Eine zweite CSI-Instanz erhält ihren eigenen Framebuffer, der aus demselben Pool allokiert wird, den die primäre verwendet. Der Pool wird geteilt; die Puffer sind unabhängig. Der Speicherbedarf des sekundären Sensors ist normalerweise viel kleiner als der des primären, da sekundäre Sensoren mit niedrigeren Auflösungen laufen, sodass der zusätzliche Speicher, den der zweite Framebuffer einnimmt, nur einen kleinen Bruchteil des primären ausmacht.
4.19.4. Der Stream-Framebuffer¶
Der Bildvorschau-Puffer ist die Ausnahme. Er wird zur Laufzeit aus keinem der Pools allokiert; er ist eine feste Region, die zur Build-Zeit reserviert wird, mit bekannter Adresse und bekannter Größe. Das hält den Vorschaupfad allen anderen Allokationen aus dem Weg – die Region existiert ab dem Boot und wird nie verschoben.
4.19.5. Der MicroPython-Heap¶
Python-Objekte – Variablen, Listen, Dictionaries, Klasseninstanzen, der Image-Wrapper, den ein snapshot()-Aufruf zurückgibt, jeder String und jedes Tupel, das die Anwendung erstellt – leben auf dem garbage-collected MicroPython-Heap, der getrennt von den Memory-Pools der Kamera ist. Der garbage-collected (GC) Heap ist ein Speicherbereich, den MicroPython selbst verwaltet: Python-Code allokiert daraus implizit bei jeder Objekterstellung, und MicroPython durchsucht den Heap regelmäßig und gibt den Speicherplatz frei, der von Objekten belegt ist, auf die die Anwendung nicht mehr verweist, sodass die Anwendung nie etwas von Hand freigeben muss.
Beim Boot wird eine eigene Region für den GC-Heap reserviert, die typischerweise im schnellen On-Chip-Speicher platziert wird, damit Python-Zugriffe schnell bleiben, mit einem optionalen Überlauf in den größeren externen Block auf Boards, die mehr Spielraum für große Datenstrukturen benötigen.
Das von snapshot() zurückgegebene Image ist ein kleines Wrapper-Objekt auf dem GC-Heap; die zugrunde liegenden Pixeldaten liegen im Framebuffer in einem der Pools der Kamera. Die beiden konkurrieren nie um denselben Speicher.
4.19.6. Alles zusammengefügt¶
Jede Art von Allokation an den richtigen Pool zu lenken – große Puffer in den größeren Pool, in den sie passen, latenzempfindliche Daten in die schnelleren Pools, den Python-Heap in seine eigene Region, die Vorschau in ihren reservierten Platz – ist das, was es ermöglicht, eine Aufnahme-Pipeline in voller Auflösung, einen Vorschaukanal und ein nicht-triviales Python-Skript nebeneinander auf Bausteinen laufen zu lassen, die insgesamt nur wenige Megabyte schnellen Speicher haben.