2.40. Memória e recolha de lixo

O MicroPython gere a memória da mesma forma que o CPython: todos os objetos vivem na heap, e um coletor de lixo (GC) liberta os objetos que nada referencia. Num dispositivo com algumas centenas de kilobytes de RAM, é por vezes necessário prestar atenção à forma como a heap está a ser utilizada; no Python para desktop, raramente é necessário.

2.40.1. A heap

A heap é a região de RAM – na maioria das câmaras OpenMV dividida por mais de um pool de memória física – que o runtime distribui pelos objetos Python. Cada vez que uma expressão Python cria um novo objeto (uma lista, uma string, um dict, um tuple, qualquer coisa que não seja um inteiro pequeno ou um singleton), um bloco de bytes é retirado da heap para o conter. Quando o GC deteta que um objeto é inacessível, devolve o bloco ao pool livre de onde veio.

Duas funções no módulo gc merecem ser conhecidas:

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

  • gc.collect() – executar um ciclo de recolha imediatamente em vez de esperar que o runtime o desencadeie.

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 compilação; a direção da mudança é o que importa: alocar objetos grandes diminui mem_free, e largar as referências mais um gc.collect devolve a memória à heap.

2.40.2. Fragmentação

O pool livre não é magicamente contíguo. À medida que os objetos entram e saem em momentos diferentes, o espaço livre fragmenta-se em pedaços cada vez menores, mesmo que o seu tamanho total continue a ser grande. Alocar um objeto maior do que o maior bloco livre individual falha com MemoryError – mesmo que haja tecnicamente RAM livre total suficiente.

Two views of the heap. Before: one large contiguous free area. After: many small free areas interleaved with allocated blocks, none individually large enough for a big allocation.

A mesma RAM livre total pode conter um buffer grande (esquerda) ou recusar (direita), dependendo do grau de fragmentação.

A fragmentação é principalmente uma preocupação em scripts de longa duração que continuam a alocar e a libertar buffers de tamanhos diferentes. A mitigação mais eficaz é pré-alocar buffers de longa duração perto do início do programa, antes que muitas alocações de curta duração tenham tido a oportunidade de as dispersar.

2.40.3. Pré-alocação

Dois padrões fazem a heap comportar-se corretamente:

  • Alocar buffers de tamanho fixo uma vez e reutilizá-los, em vez de construir uma nova lista ou bytearray por iteração.

  • Retirar constantes e tabelas de pesquisa de ciclos internos para que sejam criados 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 ciclo: cada iteração produz lixo que o GC terá de limpar mais tarde. A versão pré-alocada não produz lixo nenhum.

2.40.4. Quando chamar gc.collect

gc.collect() é normalmente automático – o runtime desencadeia-o 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 ter saído de âmbito, para os libertar imediatamente em vez de esperar que a próxima alocação pague o custo.

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

Distribuir chamadas a gc.collect por todo o lado não torna um programa mais rápido – a recolha em si demora tempo. Use-o deliberadamente, nos pontos em que o custo de uma recolha não agendada seria pior do que o custo de uma que executou propositadamente.