2.40. Memorie și colectare a gunoiului

MicroPython gestionează memoria la fel ca CPython: fiecare obiect trăiește în heap, iar un colector de gunoi (GC) eliberează obiectele pe care nimic nu le mai referă. Pe un dispozitiv cu câteva sute de kiloocteți de RAM, este uneori necesar să fii atent la modul în care este folosit heap-ul; pe Python de desktop, rareori este.

2.40.1. Heap-ul

Heap-ul este regiunea de RAM – pe majoritatea camerelor OpenMV împărțită de fapt între mai multe rezerve fizice de memorie – pe care runtime-ul o distribuie obiectelor Python. De fiecare dată când o expresie Python creează un obiect nou (o listă, un șir, un dict, un tuplu, orice nu este un întreg mic sau un singleton), un bloc de octeți iese din heap pentru a-l păstra. Când GC-ul observă că un obiect nu mai este accesibil, returnează blocul în rezerva liberă din care a provenit.

Două funcții din modulul gc merită cunoscute:

  • gc.mem_free() – numărul aproximativ de octeți liberi din heap în acest moment.

  • gc.collect() – rulează imediat un ciclu de colectare, în loc să aștepte ca runtime-ul să declanșeze unul.

import gc

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

Numerele exacte depind de versiune; direcția schimbării este ceea ce contează: alocarea de lucruri mari scade mem_free, iar renunțarea la referințe plus un gc.collect returnează heap-ul.

2.40.2. Fragmentare

Rezerva liberă nu este în mod magic contiguă. Pe măsură ce obiectele apar și dispar cu durate de viață diferite, spațiul liber se fărâmițează în bucăți tot mai mici, chiar și atunci când dimensiunea sa totală este încă mare. Alocarea unui obiect mai mare decât cea mai mare bucată liberă individuală eșuează cu MemoryError – chiar dacă din punct de vedere tehnic există suficient RAM liber total.

Două perspective asupra heap-ului. Înainte: o zonă liberă mare și contiguă. După: multe zone libere mici intercalate cu blocuri alocate, niciuna suficient de mare în mod individual pentru o alocare mare.

Aceeași cantitate totală de RAM liber poate găzdui un tampon mare (stânga) sau poate refuza acest lucru (dreapta), în funcție de cât de fragmentată este.

Fragmentarea este în principal o grijă pentru scripturile care rulează mult timp și care continuă să aloce și să elibereze tampoane de dimensiuni diferite. Cea mai eficientă măsură de atenuare este să pre-aloci tampoanele cu viață lungă aproape de începutul programului, înainte ca numeroase alocări de scurtă durată să fi avut ocazia să le împrăștie.

2.40.3. Pre-alocare

Două tipare fac heap-ul să se comporte bine:

  • Alocă tampoane de dimensiune fixă o singură dată și refolosește-le, în loc să construiești o nouă listă sau un bytearray la fiecare iterație.

  • Scoate constantele și tabelele de căutare din buclele interioare, astfel încât să fie create o singură dată.

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

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

fill(0)
fill(255)

Compară cu o versiune care creează un nou bytearray în interiorul buclei: fiecare iterație produce gunoi pe care GC-ul trebuie să îl curețe mai târziu. Versiunea pre-alocată nu produce deloc gunoi.

2.40.4. Când să apelezi gc.collect

gc.collect() este în mod normal automat – runtime-ul îl declanșează când alocările nu pot găsi suficientă memorie liberă. Apelarea lui manuală este utilă în două situații:

  • Imediat după ce un lot mare de obiecte a ieșit din domeniul de vizibilitate, pentru a le elibera imediat, în loc să aștepți ca următoarea alocare să plătească costul.

  • Chiar înaintea unei secțiuni care necesită o cantitate maximă cunoscută de memorie liberă, pentru a evita declanșarea GC-ului în mijlocul unei operațiuni sensibile la timp.

Presărarea de apeluri gc.collect peste tot nu face un program mai rapid – colectarea în sine consumă timp. Folosește-l deliberat, în punctele unde costul unei colectări neprogramate ar fi mai mare decât costul uneia pe care ai rulat-o intenționat.