Управление памятью¶
В отличие от таких языков программирования, как C/C++, MicroPython скрывает детали управления памятью от разработчика, поддерживая автоматическое управление памятью. Автоматическое управление памятью — это метод, используемый операционными системами или приложениями для автоматического управления выделением и освобождением памяти. Это устраняет такие проблемы, как забывание освободить память, выделенную для объекта. Автоматическое управление памятью также позволяет избежать критической проблемы использования памяти, которая уже была освобождена. Автоматическое управление памятью принимает множество форм, одной из которых является сборка мусора (GC).
У сборщика мусора обычно есть две обязанности:
Выделять новые объекты в доступной памяти.
Освобождать неиспользуемую память.
Существует множество алгоритмов GC, но MicroPython использует для управления памятью политику Mark and Sweep. В этом алгоритме есть фаза маркировки, которая обходит кучу, помечая все живые объекты, в то время как фаза очистки проходит по куче, освобождая все непомеченные объекты.
Функциональность сборки мусора в MicroPython доступна через встроенный модуль gc:
>>> x = 5
>>> x
5
>>> import gc
>>> gc.enable()
>>> gc.mem_alloc()
1312
>>> gc.mem_free()
2071392
>>> gc.collect()
19
>>> gc.disable()
>>>
Даже когда вызвана функция gc.disable(), сборку можно запустить с помощью gc.collect().
Память MicroPython из кода на C¶
При написании кода на C, который выделяет память из «кучи Python» (т. е. функции m_malloc(), m_malloc0(), m_free() и т. д.), необходимо учитывать работу сборщика мусора.
Фаза маркировки сборщика мусора сканирует живые указатели на память в куче, начиная со следующих корней:
Стек основной среды выполнения Python (или REPL).
Стеки каждого «потока Python» для портов, которые реализуют потоки Python поверх собственных потоков или задач операционной системы.
«Корневые указатели», определённые в коде на C с помощью макроса
MP_REGISTER_ROOT_POINTER. Это рекомендуемый способ иметь указатели со статической областью видимости на кучу Python.Отслеживаемые выделения, сделанные с помощью функций
m_tracked_calloc(),m_tracked_reallocиm_tracked_free(). Эти специальные функции позволяют выделить блок памяти, который всегда считается живым сборщиком мусора. Подобно выделению памяти в C, эта память освобождается только при вызовеm_tracked_free()или при программном сбросе. Каждое отслеживаемое выделение влечёт за собой небольшие накладные расходы по использованию памяти и времени выполнения. Эта возможность включена по умолчанию не на всех портах.
Затем сборщик мусора рекурсивно сканирует и помечает всю память, на которую указывают корневые указатели, пока все адреса не будут исчерпаны. Этого достаточно, чтобы найти все объекты Python, которые всё ещё используются средой выполнения MicroPython.
Однако следующая память не будет просканирована сборщиком мусора и может быть освобождена преждевременно:
Статические или глобальные переменные C, содержащие указатели на память в куче.
Указатели, которые указывают не на «начало» выделенного буфера (т. е. на точный адрес, возвращённый
m_malloc()), а на адрес внутри выделенного буфера (например, указатель на вложенную структуру). По соображениям производительности сборщик мусора в таких случаях не помечает охватывающий буфер.Стек любого потока или задачи RTOS, который не выполняет код Python и не зарегистрирован вручную как «поток Python» (для портов, поддерживающих собственные потоки или задачи).
Способы избежать использования после освобождения в этих сценариях:
Используйте API отслеживаемого выделения
m_tracked_calloc(),m_tracked_realloc()иm_tracked_free().Зарегистрируйте корневой указатель (см. выше) вместо хранения указателя в статической переменной.
Перестройте код, например создав API, в котором код Python инициализирует одиночный объект Python (реализованный на C), хранящий все соответствующие указатели, вместо того чтобы хранить их в статических переменных.
Примечание
Программный сброс всегда очищает кучу Python и освобождает всю память. Важно не хранить никаких указателей на кучу после программного сброса, так как они станут висячими указателями на освобождённую память.
Некоторые порты также поддерживают «кучу C» (см. Динамическое выделение памяти в C), и в этом случае вы можете выделить память, которая останется действительной после программного сброса, вызвав стандартные функции C malloc и т. д.
Модель объектов¶
Все объекты MicroPython представлены типом данных mp_obj_t. Обычно он размером со слово (т. е. того же размера, что и указатель на целевой архитектуре) и, как правило, может быть 32-битным (STM32, RP2, nRF, Unix x86) или 64-битным (Unix x64). Для некоторых представлений объектов он также может быть больше размера слова: например, OBJ_REPR_D имеет 64-битный mp_obj_t на 32-битной архитектуре.
mp_obj_t представляет объект MicroPython, например целое число, число с плавающей точкой, тип, словарь или экземпляр класса. Некоторые объекты, такие как логические значения и малые целые числа, хранят своё значение непосредственно в значении mp_obj_t и не требуют дополнительной памяти. Другие объекты хранят своё значение в другом месте памяти (например, в куче со сборкой мусора), а их mp_obj_t содержит указатель на эту память. Часть mp_obj_t представляет собой тег, который сообщает, какой это тип объекта.
Конкретные сведения о доступных представлениях см. в py/mpconfig.h.
Тегирование указателей
Поскольку указатели выровнены по словам, при их хранении в mp_obj_t младшие биты этого дескриптора объекта будут равны нулю. Например, на 32-битной архитектуре младшие 2 бита будут равны нулю:
********|********|********|******00
Эти биты зарезервированы для хранения тега. Тег хранит дополнительную информацию, в отличие от введения нового поля для хранения этой информации в объекте, что может быть неэффективным. В MicroPython тег сообщает, имеем ли мы дело с малым целым числом, интернированной (короткой) строкой или конкретным объектом, и к каждому из них применяется своя семантика.
Для малых целых чисел соответствие следующее:
********|********|********|*******1
Где звёздочки содержат фактическое целочисленное значение. Для интернированной строки или непосредственного объекта (например, True) макет значения mp_obj_t соответственно такой:
********|********|********|*****010
********|********|********|*****110
В то время как конкретный объект, не являющийся ни одним из вышеперечисленных, принимает вид:
********|********|********|******00
Звёздочки здесь соответствуют адресу конкретного объекта в памяти.
Выделение объектов¶
Значение малого целого числа хранится непосредственно в mp_obj_t и будет выделено на месте, а не в куче или где-либо ещё. Поэтому создание малых целых чисел не влияет на кучу. То же самое касается интернированных строк, текстовые данные которых уже хранятся в другом месте, и непосредственных значений, таких как None, False и True.
Всё остальное, что является конкретным объектом, выделяется в куче, и его структура устроена так, что в заголовке объекта зарезервировано поле для хранения типа объекта.
+++++++++++
+ +
+ type + object header
+ +
+++++++++++
+ + object items
+ +
+ +
+++++++++++
Наименьшая единица выделения в куче — это блок, размер которого составляет четыре машинных слова (16 байт на 32-битной машине, 32 байта на 64-битной машине). Другая структура, также выделяемая в куче, отслеживает выделение объектов в каждом блоке. Эта структура называется bitmap (битовая карта).
Битовая карта отслеживает, является ли блок «свободным» или «используемым», и использует два бита для отслеживания этого состояния для каждого блока.
Сборщик мусора по принципу mark-sweep управляет объектами, выделенными в куче, а также использует битовую карту для пометки объектов, которые всё ещё используются. Полную реализацию этих деталей см. в py/gc.c.
Выделение: компоновка кучи
Куча устроена так, что состоит из блоков в пулах. Блок может иметь различные свойства:
ATB (allocation table byte): Если установлен, то блок является обычным блоком
FREE: Свободный блок
HEAD: Начало цепочки блоков
TAIL: В хвосте цепочки блоков
MARK : Помеченный начальный блок
FTB (finaliser table byte): Если установлен, то у блока есть финализатор