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 – навіть якщо технічно достатньо загальної вільної оперативної пам’яті.

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.

Однакова загальна вільна оперативна пам’ять може вмістити великий буфер (зліва) або відмовити в цьому (справа), залежно від рівня фрагментації.

Фрагментація є проблемою переважно для скриптів із тривалим часом роботи, які постійно виділяють і звільняють буфери різних розмірів. Найефективніший спосіб пом’якшення – попереднє виділення буферів із тривалим часом існування на початку програми, поки численні короткочасні виділення не встигли їх «розсіяти».

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 скрізь не прискорює програму – збирання саме по собі займає час. Використовуйте його свідомо, у точках, де вартість незапланованого збирання є вищою за вартість запланованого.