3、内存碎片概念:内存碎片定义、内部碎片与外部碎片、碎片产生的根本原因
在嵌入式系统开发中,内存碎片是导致系统长期运行后出现分配失败、性能下降甚至崩溃的隐形杀手。对于TFLite Micro这类运行在资源极度受限的MCU上的推理引擎而言,理解内存碎片的本质是进行内存优化和碎片治理的前提。本章将系统性地剖析内存碎片的定义、分类及其产生的根本原因,为后续分析TFLite Micro的“碎片假象”与“实际占用”奠定理论基础。
3.1 内存碎片的定义与本质
内存碎片(Memory Fragmentation)是指内存空间中存在无法被有效利用的“空洞”或“浪费区域”的现象。从系统整体视角看,碎片意味着可用内存总量充足,但实际可分配给应用程序的连续内存块却不足。在TFLite Micro的上下文中,这种“总量充足但无法使用”的矛盾尤为突出——因为MCU的RAM通常只有几十到几百KB,任何形式的碎片都可能导致模型加载或推理失败。
碎片的本质是内存分配模式与释放模式在时间和空间上的不匹配。当程序以不同的顺序、不同的大小请求内存,并以非对称的方式释放时,内存空间就会被逐渐“切割”成越来越小的块。对于TFLite Micro而言,其内存分配主要发生在模型加载阶段(权重、中间张量、算子缓冲区),而释放则发生在推理结束或会话销毁时,这种“集中分配、集中释放”的模式看似不易产生碎片,但实际由于张量生命周期不同、arena复用策略等因素,碎片问题依然存在。
3.2 内部碎片与外部碎片的深入对比
内存碎片根据其产生位置和表现形式,分为内部碎片和外部碎片两大类。理解两者的区别是选择优化策略的前提。
3.2.1 内部碎片:分配单元内部的浪费
内部碎片发生在已分配的块内部。当分配器按照固定大小(如16字节、32字节对齐)或向上取整的方式分配内存时,如果请求的大小不是分配粒度的整数倍,就会在块尾部产生无法使用的剩余空间。例如,TFLite Micro的arena分配器通常按8字节或16字节对齐,若某张量需要30字节,实际分配32字节,则2字节成为内部碎片。
内部碎片的特点:
- 产生时机:在分配发生时即确定,与后续释放顺序无关。
- 大小固定:每个分配块产生的内部碎片大小是确定的,可计算。
- 不可回收:只要该块未被释放,内部碎片就无法被其他请求使用。
- 累积效应:大量小块的内部碎片累积起来,可能占据可观的内存比例。
3.2.2 外部碎片:空闲块之间的割裂
外部碎片发生在已分配块之间的空闲区域。当程序多次分配和释放不同大小的内存块后,空闲内存被分割成许多不连续的小块。虽然这些小块的总和可能满足新请求的大小需求,但由于它们不连续,无法分配给需要连续内存的请求。在TFLite Micro中,外部碎片通常出现在arena的多次复用场景中——例如,当不同算子的中间张量在不同时间点被分配和释放时,arena中会留下大小不一的空洞。
外部碎片的特点:
- 产生时机:在多次分配/释放循环后逐渐形成。
- 大小动态变化:随分配模式的变化而改变。
- 可部分回收:通过合并相邻空闲块可以恢复部分连续空间。
- 难以预测:其严重程度取决于分配序列的复杂程度。
| 对比维度 | 内部碎片 | 外部碎片 |
|---|---|---|
| 位置 | 分配块内部 | 分配块之间 |
| 产生原因 | 对齐、向上取整、固定块大小 | 分配/释放顺序不匹配、大小差异 |
| 可预测性 | 高(分配时即可计算) | 低(依赖运行时行为) |
| 回收方式 | 仅随块释放而消失 | 可通过空闲块合并回收 |
| 对TFLite Micro的影响 | arena对齐导致的字节浪费 | 多次推理后arena空洞增多 |
3.3 碎片产生的根本原因分析
碎片并非随机产生,而是由几个确定的机制驱动。理解这些根本原因,才能针对性地设计碎片抑制策略。
3.3.1 分配与释放的顺序不对称
这是外部碎片产生的首要原因。当内存块的分配顺序与释放顺序不一致时,空闲块会被“打散”。例如,假设依次分配A(100B)、B(50B)、C(80B),然后释放B,此时空闲块为50B;接着请求70B的内存,虽然总空闲空间为50B+剩余空间,但50B的块无法满足70B的请求。在TFLite Micro中,如果模型的不同算子在不同时间创建和销毁中间张量,这种不对称性就会在arena中累积。
3.3.2 请求大小的多样性
如果所有内存请求的大小都相同,那么释放后的块可以满足任意新请求,碎片几乎不会产生。但实际中,TFLite Micro的张量大小差异巨大——权重可能占用数KB,而中间激活值可能只有几十字节。这种大小差异导致释放的小块无法满足大块请求,而释放的大块又会被后续的小请求“切碎”。
3.3.3 分配器的策略选择
不同的分配策略对碎片的影响显著:
- First-fit(首次适应):分配速度快,但容易在低地址区域产生大量小碎片。
- Best-fit(最佳适应):减少大块被切碎的风险,但分配速度慢,且可能留下极小的不可用碎片。
- Buddy system(伙伴系统):通过2的幂次大小分配,合并容易,但内部碎片可能较大。
- Slab分配:针对特定大小预分配,减少碎片但灵活性差。
TFLite Micro默认使用arena(内存池)分配器,其本质是一种顺序分配+偏移复用的策略,碎片模式与通用堆分配器有显著差异。
3.3.4 生命周期差异
短期对象与长期对象共存是碎片的温床。在TFLite Micro中,模型权重是长期存在的,而推理过程中的临时张量是短期的。如果短期张量在arena中穿插分配在长期张量之间,释放后留下的空洞就无法被后续的长期分配使用,形成永久性外部碎片。
3.4 案例拆解:一个典型的碎片产生过程
为了直观理解碎片如何产生,我们模拟一个简化的TFLite Micro推理场景。假设arena大小为1024字节,初始全部空闲。
场景设定:一个包含3个算子的模型,推理过程中需要分配以下张量:
- 算子1:输入张量(128B),权重(256B),输出张量(128B)
- 算子2:输入复用算子1的输出,权重(64B),中间张量(192B),输出(128B)
- 算子3:输入复用算子2的输出,权重(32B),输出(64B)
分配过程(简化,忽略对齐):
初始:空闲[0-1023]
1. 分配算子1输入:占用[0-127],空闲[128-1023]
2. 分配算子1权重:占用[128-383],空闲[384-1023]
3. 分配算子1输出:占用[384-511],空闲[512-1023]
4. 算子1完成,释放输入[0-127]和权重[128-383],保留输出[384-511]
空闲:[0-383]连续块 + [512-1023]连续块
5. 分配算子2权重:从[0-383]中取[0-63],空闲[64-383] + [512-1023]
6. 分配算子2中间张量:从[64-383]中取[64-255],空闲[256-383] + [512-1023]
7. 分配算子2输出:从[256-383]中取[256-383](正好),空闲[512-1023]
8. 算子2完成,释放权重[0-63]和中间张量[64-255],保留输出[256-383]
空闲:[0-255] + [384-511] + [512-1023]
9. 分配算子3权重:从[0-255]取[0-31],空闲[32-255] + [384-511] + [512-1023]
10. 分配算子3输出:从[32-255]取[32-95],空闲[96-255] + [384-511] + [512-1023]
结果分析:此时arena中空闲块为[96-255](160B)、[384-511](128B)、[512-1023](512B)。如果此时有一个新请求需要200B的连续内存,虽然总空闲空间为160+128+512=800B,但最大的连续块只有512B,可以满足;但如果请求需要300B,则只有[512-1023]能满足。若请求需要600B,则分配失败——尽管总空闲空间充足。
复盘经验:
- 碎片产生的关键节点是步骤4和步骤8——释放大块后,后续的小请求将其“切碎”。
- 如果调整分配顺序(例如先分配大块),碎片程度会显著降低。
- TFLite Micro的arena可以通过离线规划(在编译时确定所有张量的偏移)来完全避免运行时碎片。
3.5 常见误区与总结复盘
在理解内存碎片时,开发者常陷入以下误区:
总结复盘:内存碎片是嵌入式系统内存管理的核心挑战之一。内部碎片源于分配粒度与请求大小的不匹配,外部碎片源于分配/释放模式的不对称。TFLite Micro的arena机制虽然避免了传统堆的许多问题,但其顺序分配和偏移复用策略也引入了独特的碎片模式。理解这些概念后,我们才能进一步分析TFLite Micro中“碎片假象”(即看起来碎片很多,但实际占用可控)与“实际占用”(即真实的内存消耗)之间的差异。