2.40. Memória e coleta de lixo

O MicroPython gerencia memória da mesma forma que o CPython: todo objeto vive no heap, e um coletor de lixo (GC) libera objetos que nada mais referencia. Em um dispositivo com algumas centenas de kilobytes de RAM, prestar atenção em como o heap está sendo usado é ocasionalmente necessário; no Python de desktop, raramente é.

2.40.1. O heap

O heap é a região da RAM – na maioria das câmeras OpenMV, na verdade dividida em mais de um pool físico de memória – que o runtime distribui para os objetos Python. Cada vez que uma expressão Python cria um novo objeto (uma lista, uma string, um dict, uma tupla, qualquer coisa que não seja um inteiro pequeno ou um singleton), um bloco de bytes sai do heap para armazená-lo. Quando o GC percebe que um objeto está inacessível, ele devolve o bloco ao pool livre de onde veio.

Duas funções no módulo gc valem a pena conhecer:

  • gc.mem_free() – número aproximado de bytes livres no heap neste momento.

  • gc.collect() – executa um ciclo de coleta imediatamente, em vez de esperar o runtime disparar um.

import gc

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

Os números exatos dependem da versão; o que importa é a direção da mudança: alocar coisas grandes diminui mem_free, e descartar as referências mais um gc.collect devolve o heap.

2.40.2. Fragmentação

O pool livre não é magicamente contíguo. Conforme os objetos surgem e desaparecem em diferentes tempos de vida, o espaço livre se quebra em pedaços cada vez menores, mesmo quando seu tamanho total ainda é grande. Alocar um objeto maior que o maior pedaço livre único falha com MemoryError – mesmo que tecnicamente haja RAM total livre suficiente.

Duas visões do heap. Antes: uma grande área livre contígua. Depois: muitas áreas livres pequenas intercaladas com blocos alocados, nenhuma individualmente grande o suficiente para uma grande alocação.

A mesma RAM total livre pode acomodar um buffer grande (esquerda) ou recusá-lo (direita), dependendo de quão fragmentada ela está.

A fragmentação é principalmente uma preocupação em scripts de longa duração que continuam alocando e liberando buffers de tamanhos diferentes. A medida de mitigação isolada mais eficaz é pré-alocar buffers de vida longa perto do início do programa, antes que muitas alocações de vida curta tenham tido a chance de espalhá-los.

2.40.3. Pré-alocação

Dois padrões fazem o heap se comportar:

  • Aloque buffers de tamanho fixo uma vez e reutilize-os, em vez de construir uma nova lista ou bytearray por iteração.

  • Tire constantes e tabelas de consulta dos laços internos para que sejam criadas uma única vez.

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

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

fill(0)
fill(255)

Compare com uma versão que cria um novo bytearray dentro do laço: cada iteração produz lixo que o GC tem que limpar depois. A versão pré-alocada não produz lixo algum.

2.40.4. Quando chamar gc.collect

gc.collect() é normalmente automático – o runtime o dispara quando as alocações não conseguem encontrar memória livre suficiente. Chamá-lo manualmente é útil em duas situações:

  • Logo após um grande lote de objetos sair de escopo, para liberá-los imediatamente em vez de esperar a próxima alocação pagar o custo.

  • Logo antes de uma seção que precisa de uma quantidade máxima conhecida de memória livre, para evitar que o GC dispare no meio de uma operação sensível ao tempo.

Espalhar chamadas a gc.collect por toda parte não torna um programa mais rápido – a coleta em si leva tempo. Use-o deliberadamente, em pontos onde o custo de uma coleta não agendada seria pior que o custo de uma que você executou de propósito.