2.40. Memoria y recolección de basura

MicroPython gestiona la memoria de la misma forma que CPython: todos los objetos viven en la memoria dinámica (heap), y un recolector de basura (GC) libera los objetos que ya no tienen ninguna referencia. En un dispositivo con unos pocos cientos de kilobytes de RAM, prestar atención a cómo se usa la memoria dinámica es necesario de vez en cuando; en el Python de escritorio, rara vez lo es.

2.40.1. La memoria dinámica (heap)

La memoria dinámica (heap) es la región de RAM (que en la mayoría de las cámaras OpenMV está repartida en realidad entre más de un grupo de memoria física) que el runtime reparte entre los objetos de Python. Cada vez que una expresión de Python crea un objeto nuevo (una lista, una cadena, un dict, una tupla, cualquier cosa que no sea un entero pequeño o un singleton), sale de la memoria dinámica un bloque de bytes para contenerlo. Cuando el GC detecta que un objeto es inalcanzable, devuelve el bloque al grupo libre del que provino.

Merece la pena conocer dos funciones del módulo gc:

  • gc.mem_free(): número aproximado de bytes libres en la memoria dinámica en este momento.

  • gc.collect(): ejecuta un ciclo de recolección de inmediato en lugar de esperar a que el runtime lo active.

import gc

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

Las cifras exactas dependen de la compilación; lo que importa es la dirección del cambio: asignar cosas grandes reduce mem_free, y soltar las referencias junto con un gc.collect devuelve la memoria dinámica.

2.40.2. Fragmentación

El grupo libre no es mágicamente contiguo. A medida que los objetos aparecen y desaparecen con distintos tiempos de vida, el espacio libre se va dividiendo en fragmentos cada vez más pequeños, aunque su tamaño total siga siendo grande. Asignar un objeto mayor que el mayor fragmento libre individual falla con MemoryError, aunque técnicamente haya suficiente RAM libre total.

Dos vistas de la memoria dinámica. Antes: una única zona libre grande y contigua. Después: muchas zonas libres pequeñas intercaladas con bloques asignados, ninguna individualmente lo bastante grande para una asignación grande.

La misma RAM libre total puede albergar un búfer grande (izquierda) o negarse a ello (derecha) según lo fragmentada que esté.

La fragmentación es sobre todo una preocupación en los scripts de larga ejecución que no dejan de asignar y liberar búferes de distintos tamaños. La medida de mitigación más eficaz, con diferencia, es preasignar los búferes de larga duración cerca del inicio del programa, antes de que muchas asignaciones de corta duración hayan tenido ocasión de dispersarlos.

2.40.3. Preasignación

Dos patrones hacen que la memoria dinámica se comporte:

  • Asigna búferes de tamaño fijo una sola vez y reutilízalos, en lugar de construir una lista o un bytearray nuevos en cada iteración.

  • Saca las constantes y las tablas de búsqueda de los bucles internos para que se creen una sola vez.

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

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

fill(0)
fill(255)

Compáralo con una versión que crea un bytearray nuevo dentro del bucle: cada iteración produce basura que el GC tendrá que limpiar más tarde. La versión preasignada no genera basura en absoluto.

2.40.4. Cuándo llamar a gc.collect

gc.collect() normalmente es automático: el runtime lo activa cuando las asignaciones no encuentran suficiente memoria libre. Llamarlo a mano resulta útil en dos situaciones:

  • Justo después de que un gran lote de objetos haya salido del ámbito, para liberarlos de inmediato en lugar de esperar a que la siguiente asignación pague el coste.

  • Justo antes de una sección que necesita una cantidad máxima conocida de memoria libre, para evitar que el GC se dispare a mitad de una operación sensible al tiempo.

Salpicar llamadas a gc.collect por todas partes no hace que un programa sea más rápido: la recolección en sí lleva tiempo. Úsalo de forma deliberada, en los puntos donde el coste de una recolección no programada sería peor que el de una que ejecutaste a propósito.