2.40. Память и сборка мусора

MicroPython управляет памятью так же, как CPython: каждый объект живёт в куче, а сборщик мусора (GC) освобождает объекты, на которые больше ничего не ссылается. На устройстве с несколькими сотнями килобайт ОЗУ иногда необходимо следить за тем, как используется куча; на настольном Python это требуется редко.

2.40.1. Куча

Куча – это область ОЗУ (на большинстве камер OpenMV фактически разделённая на несколько физических пулов памяти), которую среда выполнения выделяет объектам Python. Каждый раз, когда выражение Python создаёт новый объект (список, строку, словарь, кортеж – что угодно, кроме малого целого числа или синглтона), из кучи выделяется блок байтов для его хранения. Когда GC замечает, что объект недостижим, он возвращает блок в тот свободный пул, из которого он был взят.

Стоит знать две функции из модуля gc:

  • gc.mem_free() – приблизительное количество свободных байтов в куче прямо сейчас.

  • gc.collect() – немедленно запустить цикл сборки вместо ожидания, пока среда выполнения сама его инициирует.

import gc

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

Точные числа зависят от сборки; важно направление изменения: выделение крупных объектов уменьшает mem_free, а удаление ссылок плюс gc.collect возвращает память в кучу.

2.40.2. Фрагментация

Свободный пул не становится непрерывным по волшебству. Поскольку объекты появляются и исчезают с разным временем жизни, свободное пространство дробится на всё более мелкие куски, даже когда его общий размер всё ещё велик. Выделение объекта, который больше самого большого отдельного свободного куска, завершается с ошибкой MemoryError – даже если технически общего свободного ОЗУ достаточно.

Два вида кучи. До: одна большая непрерывная свободная область. После: множество мелких свободных областей, чередующихся с выделенными блоками, ни одна из которых по отдельности не достаточно велика для крупного выделения.

Одно и то же общее свободное ОЗУ может вместить большой буфер (слева) или отказаться (справа) в зависимости от того, насколько оно фрагментировано.

Фрагментация в основном вызывает беспокойство в долго работающих скриптах, которые продолжают выделять и освобождать буферы разного размера. Самая эффективная мера противодействия – предварительно выделить долгоживущие буферы ближе к началу программы, прежде чем множество короткоживущих выделений успеет их разбросать.

2.40.3. Предварительное выделение

Два приёма заставляют кучу вести себя хорошо:

  • Выделите буферы фиксированного размера один раз и переиспользуйте их, вместо создания нового списка или bytearray на каждой итерации.

  • Вынесите константы и таблицы поиска за пределы внутренних циклов, чтобы они создавались один раз.

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

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

fill(0)
fill(255)

Сравните с версией, которая создаёт новый bytearray внутри цикла: каждая итерация порождает мусор, который GC потом приходится убирать. Версия с предварительным выделением не порождает мусора вовсе.

2.40.4. Когда вызывать gc.collect

gc.collect() обычно срабатывает автоматически – среда выполнения запускает его, когда выделения не могут найти достаточно свободной памяти. Вызывать его вручную полезно в двух ситуациях:

  • Сразу после того, как большая партия объектов вышла из области видимости, чтобы освободить их немедленно, а не ждать, пока следующее выделение оплатит эту цену.

  • Непосредственно перед участком, которому нужен известный максимальный объём свободной памяти, чтобы избежать срабатывания GC посреди операции, чувствительной ко времени.

Расстановка вызовов gc.collect повсюду не делает программу быстрее – сама сборка занимает время. Используйте его обдуманно, в тех местах, где цена незапланированной сборки была бы хуже цены той, что вы запустили намеренно.