2.40. Memória és szemétgyűjtés

A MicroPython ugyanúgy kezeli a memóriát, mint a CPython: minden objektum a heap-en él, és egy szemétgyűjtő (GC) felszabadítja azokat az objektumokat, amelyekre már semmi sem hivatkozik. Egy néhány száz kilobájt RAM-mal rendelkező eszközön időnként szükséges figyelmet fordítani arra, hogyan használjuk a heap-et; asztali Python-on erre ritkán van szükség.

2.40.1. A heap

A heap a RAM azon területe – a legtöbb OpenMV kamerán valójában több fizikai memóriakészlet között elosztva –, amelyet a futtatókörnyezet a Python objektumoknak oszt ki. Valahányszor egy Python kifejezés új objektumot hoz létre (listát, karakterláncot, dict-et, tuple-t, bármit, ami nem kis egész szám vagy szingleton), egy bájtblokk kerül ki a heap-ből, hogy tárolja azt. Amikor a GC észreveszi, hogy egy objektum elérhetetlen, visszaadja a blokkot abba a szabad készletbe, ahonnan származott.

A gc modul két függvényét érdemes ismerni:

  • gc.mem_free() – a heap-en éppen szabad bájtok hozzávetőleges száma.

  • gc.collect() – azonnal lefuttat egy gyűjtési ciklust, ahelyett, hogy megvárná, amíg a futtatókörnyezet kiváltja egyet.

import gc

print("before:", gc.mem_free())
big = [0] * 10000
print("after :", gc.mem_free())
del big
gc.collect()
print("freed :", gc.mem_free())

A pontos számok a buildtől függenek; a változás iránya az, ami számít: nagy dolgok lefoglalása csökkenti a mem_free értéket, a hivatkozások elejtése plusz egy gc.collect pedig visszaadja a heap-et.

2.40.2. Töredezettség

A szabad készlet nem varázslatos módon összefüggő. Ahogy az objektumok különböző élettartamok mellett jönnek és mennek, a szabad terület egyre kisebb és kisebb darabokra törik, még akkor is, ha teljes mérete továbbra is nagy. Egy a legnagyobb egyetlen szabad darabnál nagyobb objektum lefoglalása MemoryError hibával bukik el – noha technikailag elegendő összesen szabad RAM van.

A heap két nézete. Előtte: egy nagy, összefüggő szabad terület. Utána: sok kis szabad terület, lefoglalt blokkokkal váltakozva, egyik sem elég nagy önmagában egy nagy lefoglaláshoz.

Ugyanaz az összesen szabad RAM be tud fogadni egy nagy puffert (balra), vagy elutasítja azt (jobbra) attól függően, mennyire töredezett.

A töredezettség többnyire hosszan futó szkripteknél jelent gondot, amelyek folyamatosan különböző méretű puffereket foglalnak le és szabadítanak fel. A leghatékonyabb enyhítés az, ha a hosszú életű puffereket előre lefoglaljuk a program elejéhez közel, mielőtt rengeteg rövid életű lefoglalásnak alkalma lett volna szétszórni őket.

2.40.3. Előre lefoglalás

Két minta szabályozott viselkedésre bírja a heap-et:

  • Foglalj le rögzített méretű puffereket egyszer, és használd újra őket, ahelyett, hogy minden iterációban új listát vagy bytearray objektumot építenél.

  • Húzd ki a konstansokat és a kereső táblákat a belső ciklusokból, hogy egyszer jöjjenek létre.

buf = bytearray(64)        # one allocation, reused below

def fill(value):
    for i in range(len(buf)):
        buf[i] = value

fill(0)
fill(255)

Hasonlítsd össze egy olyan változattal, amely a cikluson belül hoz létre új bytearray-t: minden iteráció szemetet termel, amelyet a GC-nek később ki kell takarítania. Az előre lefoglalt változat egyáltalán nem termel szemetet.

2.40.4. Mikor hívjuk a gc.collect-ot

A gc.collect() rendszerint automatikus – a futtatókörnyezet akkor váltja ki, amikor a lefoglalások nem találnak elég szabad memóriát. Kézzel meghívni két helyzetben hasznos:

  • Közvetlenül azután, hogy egy nagy adag objektum hatókörén kívülre került, hogy azonnal felszabaduljanak, ahelyett, hogy megvárnánk, amíg a következő lefoglalás állja a költséget.

  • Közvetlenül egy olyan szakasz előtt, amelynek ismert maximális mennyiségű szabad memóriára van szüksége, hogy elkerüljük, hogy a GC egy időérzékeny művelet közepén induljon el.

Ha mindenhová gc.collect hívásokat szórsz, az nem teszi gyorsabbá a programot – maga a gyűjtés időbe telik. Használd átgondoltan, olyan pontokon, ahol egy nem ütemezett gyűjtés költsége rosszabb lenne, mint egy szándékosan lefuttatotté.