4.19. Пулы памяти

Камера, которая удерживает три полноразмерных кадра в пуле буфера кадра, параллельно использует отдельный буфер предпросмотра и при этом ещё оставляет место для скрипта на Python и его объектов, оперирует большим объёмом памяти, чем мог бы предоставить один блок RAM на MCU. MicroPython умещает всё это, распределяя данные по нескольким различным видам памяти, которые предоставляет MCU, и направляя каждый тип выделения именно в тот вид памяти, который ему действительно нужен.

4.19.1. Виды памяти

Современный MCU камеры OpenMV Cam предоставляет четыре различных вида памяти. Первый невидим для приложения; остальные три являются пулами, из которых могут производиться выделения.

  • Кэш данных CPU – небольшая, очень быстрая область памяти, расположенная между CPU и остальной RAM. Когда CPU читает или записывает значение из основной памяти, кэш автоматически сохраняет его копию, поэтому повторные обращения к тем же данным остаются в кэше и не платят за выход в более медленную память. Кэш не является пулом, из которого производятся выделения. Он прозрачен для приложения – он просто делает остальную RAM на практике быстрее, чем можно было бы ожидать по её необработанной задержке, до тех пор пока рабочий набор данных в него помещается.

  • Тесно связанная с процессором память – небольшой блок RAM, подключённый напрямую к CPU без шины между ними. Доступ за один такт, без промахов, без ожиданий. Из этого пула производятся выделения, которым действительно нужна самая быстрая возможная память – где важен каждый такт задержки.

  • Быстрая встроенная память – от нескольких сотен килобайт до примерно мегабайта RAM, встроенной в корпус MCU. Низкая задержка, высокая пропускная способность, но ограниченный размер. Здесь находится куча MicroPython, чтобы доступ к объектам Python оставался быстрым; меньшие рабочие буферы, к которым CPU обращается часто, делят этот пул.

  • Более медленная объёмная память – на платах, где MCU сочетается с внешним кристаллом памяти, десятки мегабайт внешней RAM, доступной через внешнюю шину. Намного больше по объёму, но каждый доступ занимает больше времени, чем во встроенной памяти; кэш данных скрывает значительную часть этой стоимости для рабочих наборов, которые он может удержать, и разрыв проявляется на операциях, которые проходят по данным, слишком большим для кэширования. Используется для выделений, которые должны быть большими и при которых CPU может терпеть меньшую скорость – в первую очередь для пула буфера кадра.

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

4.19.2. Основной буфер кадра

Буфер кадра, обеспечивающий работу snapshot(), не запрашивает быструю память. Он запрашивает достаточно памяти – не более того. Это помещает его в тот пул, который является самым большим, поэтому на плате со встроенной и внешней памятью буфер кадра оказывается во внешнем блоке.

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

Точный размер, который занимает буфер кадра, выбирается на основе текущих значений pixformat(), framesize() и количества framebuffers(); он увеличивается или уменьшается каждый раз, когда что-либо из этого меняется.

4.19.3. Вторичные буферы кадра датчиков

Второй экземпляр CSI получает собственный буфер кадра, выделяемый из того же пула, который использует основной. Пул общий; буферы независимы. Объём, занимаемый вторичным, обычно намного меньше, чем у основного, потому что вторичные датчики работают на более низких разрешениях, поэтому дополнительная память, которую занимает второй буфер кадра, составляет небольшую долю от объёма основного.

4.19.4. Буфер кадра потока

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

4.19.5. Куча MicroPython

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

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

Объект Image, возвращаемый методом snapshot(), – это небольшая обёртка в GC-куче; лежащие в его основе пиксельные данные находятся в буфере кадра в одном из пулов камеры. Эти двое никогда не конкурируют за одну и ту же память.

4.19.6. Собираем всё вместе

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