2.40. Memoria e garbage collection

MicroPython gestisce la memoria nello stesso modo di CPython: ogni oggetto vive nell”heap, e un garbage collector (GC) libera gli oggetti a cui nulla fa più riferimento. Su un dispositivo con poche centinaia di kilobyte di RAM, prestare attenzione a come viene usato l’heap è occasionalmente necessario; sul Python desktop, lo è raramente.

2.40.1. L’heap

L’heap è la regione di RAM – sulla maggior parte delle camere OpenMV in realtà suddivisa su più pool fisici di memoria – che il runtime distribuisce agli oggetti Python. Ogni volta che un’espressione Python crea un nuovo oggetto (una lista, una stringa, un dict, una tupla, qualsiasi cosa che non sia un piccolo intero o un singleton), un blocco di byte viene prelevato dall’heap per contenerlo. Quando il GC nota che un oggetto è irraggiungibile, restituisce il blocco al pool libero da cui proveniva.

Due funzioni del modulo gc vale la pena conoscere:

  • gc.mem_free() – numero approssimativo di byte liberi nell’heap in questo momento.

  • gc.collect() – esegue immediatamente un ciclo di collection anziché attendere che il runtime ne attivi uno.

import gc

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

I numeri esatti dipendono dalla build; ciò che conta è la direzione della variazione: allocare oggetti grandi diminuisce mem_free, e rilasciare i riferimenti più un gc.collect restituisce l’heap.

2.40.2. Frammentazione

Il pool libero non è magicamente contiguo. Man mano che gli oggetti vanno e vengono con tempi di vita diversi, lo spazio libero si frammenta in pezzi sempre più piccoli anche quando la sua dimensione totale è ancora grande. Allocare un oggetto più grande del singolo pezzo libero più grande fallisce con MemoryError – anche se tecnicamente c’è abbastanza RAM libera totale.

Due viste dell'heap. Prima: un'unica grande area libera contigua. Dopo: molte piccole aree libere intercalate con blocchi allocati, nessuna abbastanza grande da sola per una grande allocazione.

La stessa RAM libera totale può contenere un buffer grande (sinistra) o rifiutarsi di farlo (destra) a seconda di quanto è frammentata.

La frammentazione è soprattutto un problema negli script di lunga durata che continuano ad allocare e liberare buffer di dimensioni diverse. La mitigazione più efficace in assoluto è pre-allocare i buffer a vita lunga vicino all’inizio del programma, prima che molte allocazioni a vita breve abbiano avuto la possibilità di disperderli.

2.40.3. Pre-allocazione

Due approcci fanno comportare bene l’heap:

  • Alloca i buffer a dimensione fissa una sola volta e riutilizzali, invece di costruire una nuova lista o un nuovo bytearray a ogni iterazione.

  • Estrai le costanti e le tabelle di lookup dai cicli interni in modo che vengano create una sola volta.

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

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

fill(0)
fill(255)

Confronta con una versione che crea un nuovo bytearray all’interno del ciclo: ogni iterazione produce garbage che il GC deve ripulire in seguito. La versione pre-allocata non produce alcun garbage.

2.40.4. Quando chiamare gc.collect

gc.collect() è normalmente automatico – il runtime lo attiva quando le allocazioni non trovano abbastanza memoria libera. Chiamarlo manualmente è utile in due situazioni:

  • Subito dopo che un grande lotto di oggetti è uscito dall’ambito, per liberarli immediatamente anziché attendere che la prossima allocazione ne paghi il costo.

  • Subito prima di una sezione che necessita di una quantità massima nota di memoria libera, per evitare che il GC scatti a metà di un’operazione sensibile al tempo.

Disseminare chiamate a gc.collect ovunque non rende un programma più veloce – la collection stessa richiede tempo. Usala in modo deliberato, nei punti in cui il costo di una collection non programmata sarebbe peggiore del costo di una eseguita di proposito.