4.19. 内存池¶
一台摄像头要在帧缓冲区池中保存三帧全分辨率图像、同时运行一个独立的预览缓冲区,还要为 Python 脚本及其对象留出空间,它所要调度的内存量超过了 MCU 上单块 RAM 所能提供的容量。MicroPython 之所以能把这一切装下,靠的是将它们分散到 MCU 提供的 几种不同类型的内存 上,并把每一类分配请求路由到它实际所需的那种内存上。
4.19.1. 内存的种类¶
现代 OpenMV Cam 的 MCU 提供四种不同类型的内存。第一种对应用程序是不可见的;其余三种是分配请求可以从中取用的内存池。
CPU 的数据缓存 —— 位于 CPU 与其余 RAM 之间的一小块极快内存区域。当 CPU 从主存读取或写入某个值时,缓存会自动保留一份副本,因此对同一数据的重复访问会停留在缓存中,永远不必付出访问较慢内存的代价。该缓存 并不是 分配请求可以从中取用的内存池。它对应用程序是透明的 —— 它只是让其余 RAM 在实际使用中感觉比其原始延迟所暗示的更快,直到工作集大到无法再装进缓存为止。
紧耦合处理器内存 —— 一小块直接连到 CPU、中间没有总线的 RAM。单周期访问,从不未命中,从不等待。那些确实需要尽可能最快内存的分配 —— 即每一个延迟周期都至关重要的场合 —— 都从这个内存池中取用。
片上快速内存 —— 内建于 MCU 封装中、从几百千字节到约一兆字节的 RAM。低延迟、高带宽,但容量有限。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 对象 —— 变量、列表、字典、类实例、snapshot() 调用所返回的 Image 包装对象,以及应用程序创建的每一个字符串和元组 —— 都驻留在 MicroPython 垃圾回收堆 上,该堆 独立 于摄像头的内存池。这个垃圾回收(GC)堆是 MicroPython 自行管理的一块内存区域:每当创建一个对象时,Python 代码都会隐式地从中分配,而 MicroPython 会周期性地扫描该堆,回收那些应用程序不再引用的对象所占用的空间,因此应用程序从不需要手动释放任何东西。
启动时会为 GC 堆划出一块专用区域,通常放置在片上快速内存中以保持 Python 访问的快速性;在那些需要为大型数据结构留出更多余量的开发板上,还可以选择溢出到较大的外部内存块中。
snapshot() 返回的 Image 是 GC 堆上的一个小型包装对象;其底层的像素数据则存放在摄像头某个内存池中的帧缓冲区里。二者从不争用同一块内存。
4.19.6. 把它们组合起来¶
把每一类分配引导到正确的内存池 —— 大缓冲区放到能容纳它们的较大池中,对延迟敏感的数据放到较快的池中,Python 堆放到它自己的区域,预览放到它预留的槽位 —— 正是这一点,使得在总共只有几兆字节快速内存的器件上,能够让全分辨率采集流水线、预览通道和一个相当复杂的 Python 脚本彼此并行运行成为可能。