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.

Two views of the heap. Before: one large contiguous free area. After: many small free areas interleaved with allocated blocks, none individually large enough for a big allocation.

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é.