Керування пам’яттю¶
На відміну від мов програмування, таких як 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.
Bitmap відстежує, чи є блок «вільним» чи «у використанні», і використовує два біти для відстеження цього стану для кожного блоку.
Збирач сміття типу «позначити та підмести» керує об’єктами, виділеними на купі, а також використовує bitmap для позначення об’єктів, які ще використовуються. Дивіться py/gc.c для повної реалізації цих деталей.
Виділення: розміщення купи
Купа влаштована так, що складається з блоків у пулах. Блок може мати різні властивості:
ATB(allocation table byte): Якщо встановлено, блок є звичайним блоком
FREE: Вільний блок
HEAD: Початок ланцюжка блоків
TAIL: У хвості ланцюжка блоків
MARK : Позначений початковий блок
FTB(finaliser table byte): Якщо встановлено, блок має фіналізатор