Správa RAM a předcházení fragmentaci heapu na CircuitPythonu
Toto je obecná poznámka o paměti v CircuitPythonu / MicroPythonu (není specifická pro hry) — platí pro jakýkoli dlouho běžící program, který pracuje s velkými buffery: grafické plochy, síťové odpovědi, souborové/audio streamy, parsování dat atd. Technika na konci (předalokovaná arena) je široce použitelná.
Past: gc.mem_free() lže (není to největší volný blok)
Sekce “Past: gc.mem_free() lže (není to největší volný blok)”MicroPython/CircuitPython používají nepřesouvající mark-and-sweep GC: uvolňuje nedosažitelné
objekty, ale nikdy nepřesouvá živé (objekty jsou odkazovány surovými ukazateli a
C stack se prohledává konzervativně — takže je bezpečně přemístit nelze). Sousední
volné bloky se při gc.collect() sloučí, ale volné místo rozdělené živými objekty
zůstane rozdělené.
Důsledek: poté, co program alokoval a uvolnil mnoho různě velkých bufferů, heap fragmentuje. Můžete mít spoustu celkové volné RAM, ale žádný jediný souvislý blok dostatečně velký pro další velkou alokaci:
gc.mem_free() -> 90000 # 90 KB volných...bytearray(51200) # ...ale tohle vyhodí MemoryError (žádný souvislý úsek 51 KB)gc.mem_free() je celkové volné místo; co velká alokace potřebuje, je největší souvislý
volný blok, který může být mnohem menší a který se s fragmentací session zmenšuje.
Měření největšího souvislého bloku
Sekce “Měření největšího souvislého bloku”Žádná vestavěná funkce na to není; najděte ho binárním vyhledáváním (po gc.collect()):
import gcdef largest_block(): gc.collect() lo, hi = 0, gc.mem_free() while hi - lo > 256: m = (lo + hi) // 2 try: b = bytearray(m); del b; lo = m except MemoryError: hi = m gc.collect() return loimport micropython; micropython.mem_info(1) vypíše kompletní mapu heapu (co je živé a
kde), pokud potřebujete vidět, proč je fragmentovaný.
Kdy to zabolí
Sekce “Kdy to zabolí”Jakýkoli vzor, který opakovaně alokuje a uvolňuje velký buffer během jednoho běhu:
- Networking / web: čtení HTTP odpovědi, JSON/MQTT payloadu, TLS recordu, stažení obrázku — každý požadavek nabírá (a uvolňuje) čerstvý kilobajtový buffer.
- Zpracování souborů / streamů: čtení souboru po částech, dekomprese, parsování.
- Audio: sample buffery pro jednotlivé klipy.
- Grafika: celoobrazovkové/velké kreslicí plochy (např.
displayio/picogameCanvas) vytvářené na obrazovku/úroveň.
Jeden velký buffer alokovaný jednou při bootu a držený navždy je v pořádku (dostane souvislý blok, dokud je heap čerstvý). Problém je v churnu.
Řešení: předalokovaná arena
Sekce “Řešení: předalokovaná arena”Naberte jeden velký buffer jednou, brzy (když je heap čerstvý a souvislý) a pak rozdávejte jeho výřezy pro velké krátkodobé buffery. Ty buffery pak nikdy nealokují/neuvolňují za běhu, takže nemohou nic fragmentovat. Stejné bajty areny znovu používejte pro práci, která se časově nepřekrývá.
lib/picogame_arena.py je malá obecná implementace (je v picogame knihovně, ale
třída Arena není specifická pro hry):
import picogame_arenaAR = picogame_arena.Arena(4096) # 4096 bajtů, nabráno předem (velikost = vaše maximum)
# --- síťový příklad: znovu použij JEDEN response buffer místo churnu ---buf = AR.alloc(4096) # memoryview výřez, žádná alokace na požadavekwhile True: AR.reset() # znovu použij stejné bajty pro každý požadavek n = sock.recv_into(buf) # čti přímo do výřezu areny process(buf[:n]) # parsuj bez alokace dalšího velkého bufferu# --- grafický příklad (picogame): podlož velké Canvasy pamětí areny ---AR = picogame_arena.Arena(320 * 80) # pixely (×2 bajty); největší plocha, kterou potřebuješAR.reset(); road = AR.canvas(320, 80) # velká plocha jedné obrazovky# později jiná obrazovka (která není živá současně) znovu použije stejnou arenu:AR.reset(); shapes = AR.canvas(320, 44); btn = AR.canvas(160, 48)API: Arena(pixels) (alokuje pixels*2 bajtů), alloc(nbytes) -> memoryview,
canvas(w, h, transparent=None) -> Canvas (vyžaduje firmwarový argument Canvas(..., buffer=)),
reset() (převine kurzor — volejte na začátku každého nepřekrývajícího se použití),
free().
Klíčová pointa: arena způsobí, že se velká alokace stane jednou při startu a výřezy se nikdy nedotknou heapu — takže session může běžet neomezeně dlouho bez selhání „90 KB volných, ale nelze alokovat 51 KB”.
Další techniky (kombinujte podle potřeby)
Sekce “Další techniky (kombinujte podle potřeby)”- Alokujte velké/dlouho žijící buffery jako první, při bootu a držte je — neuvolňujte a nevytvářejte je znovu při každé iteraci.
- Object pooly pro mnoho malých stejně velkých objektů (např. sprity, požadavky) — znovu
je používejte místo churnu alloc/free. (picogame:
picogame_pool.) recv_into/readinto(a podobná*_intoAPI) čtou do existujícího bufferu místo alokace nového bytes objektu při každém volání.gc.collect()na přirozených hranicích (konec požadavku/úrovně) pro sloučení sousedních volných bloků — nutné, ale ne dostačující (nemůže přesouvat živé objekty).gc.threshold(n)pro spuštění GC dříve a udržení heapu úhlednějšího.- CircuitPython už přesouvá „long-lived” objekty z importu na konec heapu při prvním GC, čímž drží spodní heap souvislý pro pracovní alokace — takže import vašich modulů předem (ne líně, uprostřed běhu) pomáhá.
Proč prostě nedefragmentovat?
Sekce “Proč prostě nedefragmentovat?”Skutečný komprimující/defragmentující GC není proveditelný jako doplněk: objekty MicroPythonu se navzájem odkazují surovými ukazateli (v Pythonu, v C modulech, v bytecodu) a GC prohledává C stack konzervativně — takže nemůže bezpečně přesunout objekt a přepsat každý odkaz na něj. To by vyžadovalo jiný (přesný / handle-based) objektový model v jádru VM. Vzor s arenou je praktická odpověď: nenechte velké buffery churnovat na prvním místě.
Viz také argument Canvas(..., buffer=) enginu (podložte kreslicí plochu pamětí areny)
a pomocníka picogame_pool (object pooly). Buildněte a měřte nejprve v desktopovém
simulátoru; optimalizujte až poté, co jste změřili, kam vlastně RAM mizí.