2.40. Mémoire et ramasse-miettes

MicroPython gère la mémoire de la même manière que CPython : chaque objet réside sur le tas (heap), et un ramasse-miettes (garbage collector, GC) libère les objets que plus rien ne référence. Sur un appareil disposant de quelques centaines de kilo-octets de RAM, il est parfois nécessaire de prêter attention à la façon dont le tas est utilisé ; sur le Python de bureau, ça l’est rarement.

2.40.1. Le tas

Le tas est la région de la RAM – sur la plupart des caméras OpenMV, en réalité répartie sur plusieurs réservoirs de mémoire physiques – que le runtime distribue aux objets Python. Chaque fois qu’une expression Python crée un nouvel objet (une liste, une chaîne, un dictionnaire, un tuple, tout ce qui n’est pas un petit entier ou un singleton), un bloc d’octets est prélevé sur le tas pour le contenir. Lorsque le GC constate qu’un objet est inaccessible, il rend le bloc au réservoir libre dont il provenait.

Deux fonctions du module gc méritent d’être connues :

  • gc.mem_free() – nombre approximatif d’octets libres sur le tas à l’instant présent.

  • gc.collect() – exécute immédiatement un cycle de ramasse-miettes au lieu d’attendre que le runtime en déclenche un.

import gc

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

Les chiffres exacts dépendent de la version ; c’est le sens du changement qui importe : allouer de gros objets diminue mem_free, et abandonner les références plus un gc.collect rend la mémoire au tas.

2.40.2. Fragmentation

Le réservoir libre n’est pas magiquement contigu. Au fur et à mesure que les objets vont et viennent avec des durées de vie différentes, l’espace libre se morcelle en fragments de plus en plus petits, même quand sa taille totale reste importante. Allouer un objet plus gros que le plus grand fragment libre unique échoue avec une MemoryError – même s’il y a techniquement assez de RAM libre au total.

Deux vues du tas. Avant : une grande zone libre contiguë. Après : de nombreuses petites zones libres entrelacées avec des blocs alloués, aucune individuellement assez grande pour une allocation importante.

La même quantité totale de RAM libre peut accueillir un gros tampon (à gauche) ou le refuser (à droite) selon son degré de fragmentation.

La fragmentation est surtout une préoccupation pour les scripts à longue durée d’exécution qui ne cessent d’allouer et de libérer des tampons de tailles différentes. La mesure d’atténuation la plus efficace est de préallouer les tampons à longue durée de vie près du début du programme, avant que de nombreuses allocations à courte durée de vie n’aient eu l’occasion de les disperser.

2.40.3. Préallocation

Deux approches font bien se comporter le tas :

  • Allouez des tampons de taille fixe une seule fois et réutilisez-les, au lieu de construire une nouvelle liste ou un nouveau bytearray à chaque itération.

  • Sortez les constantes et les tables de correspondance des boucles internes afin qu’elles ne soient créées qu’une fois.

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

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

fill(0)
fill(255)

Comparez avec une version qui crée un nouveau bytearray à l’intérieur de la boucle : chaque itération produit des déchets que le GC devra nettoyer plus tard. La version préallouée ne produit aucun déchet.

2.40.4. Quand appeler gc.collect

gc.collect() est normalement automatique – le runtime le déclenche quand les allocations ne trouvent pas assez de mémoire libre. L’appeler manuellement est utile dans deux situations :

  • Juste après qu’un gros lot d’objets soit sorti de la portée, pour les libérer immédiatement au lieu d’attendre que la prochaine allocation en paie le coût.

  • Juste avant une section qui nécessite un maximum connu de mémoire libre, pour éviter que le GC ne se déclenche en plein milieu d’une opération sensible au temps.

Parsemer des appels gc.collect partout ne rend pas un programme plus rapide – le ramasse-miettes prend lui-même du temps. Utilisez-le délibérément, aux endroits où le coût d’un ramasse-miettes non planifié serait pire que celui d’un que vous avez exécuté volontairement.