4、代码裁剪基础:使用编译器选项(如-ffunction-sections、-fdata-sections)和链接器垃圾回收(--gc-sections)减少无用代码
TFLite Micro 在嵌入式平台上的 Flash 占用通常在 50KB 到 200KB 之间,而许多 MCU 的片上 Flash 仅有 64KB 或 128KB。面对这样的容量约束,代码裁剪是最直接、成本最低的优化手段。本章聚焦于编译器与链接器层面的裁剪技术——通过 -ffunction-sections、-fdata-sections 和 --gc-sections 的组合,将未使用的函数和数据从最终固件中剔除。这三项选项的配合使用,往往能减少 20% 到 40% 的 Flash 占用,且几乎不需要修改任何业务代码。
上图展示了从源码到最终固件的完整裁剪链路。编译器负责将每个函数和全局变量放入独立的段(section),链接器则根据入口点(如 Reset_Handler)进行可达性分析,回收所有未被引用的段。这一机制的核心思想是:让链接器能够“看见”哪些代码是真正需要的,从而精确剔除无用部分。
4.1 核心原理:段(Section)与垃圾回收机制
在默认编译模式下,编译器会将所有函数和全局变量放入统一的 .text 和 .data 段中。这意味着链接器只能以整个段为单位进行链接——只要有一个函数被引用,整个 .text 段中的所有函数都会被链接进固件,即使其中 90% 的函数从未被调用。
启用 -ffunction-sections 后,编译器为每个函数生成独立的段,命名格式为 .text.function_name。同理,-fdata-sections 为每个全局变量生成独立的 .data.variable_name 段。此时链接器获得了“细粒度”的控制能力,可以结合 --gc-sections 选项,从入口点开始进行引用遍历,将所有未被引用的段标记为“垃圾”并从最终镜像中移除。
关键理解:这三项选项的组合本质上是将“链接粒度”从“文件级”降低到“函数级”。默认情况下,链接器以目标文件(.o)为最小单位;启用这些选项后,最小单位变为单个函数或单个变量。粒度越细,可裁剪的空间就越大。
值得注意的是,--gc-sections 的回收依据是“引用关系”,而非“是否被 main 函数直接调用”。只要某个函数被任何保留的函数所引用,它就会被保留。因此,中断处理函数、回调函数、构造函数等特殊入口点需要额外注意——它们可能没有显式的调用者,但必须被保留。
4.2 操作流程:在 TFLite Micro 工程中启用裁剪
在 TFLite Micro 的构建系统中,通常使用 CMake 或 Makefile 管理编译选项。以下是在典型 ARM Cortex-M 工程中的配置步骤:
- 修改编译选项:在 CMakeLists.txt 或 Makefile 中,为 C 和 C++ 编译器添加
-ffunction-sections -fdata-sections。 - 修改链接选项:为链接器添加
--gc-sections(GCC 工具链)或--remove-section(ARMCC 工具链)。 - 检查链接脚本:确保链接脚本(.ld 文件)中的段定义与编译器生成的段名匹配。通常需要添加
KEEP()指令保护关键段。 - 验证裁剪效果:编译后使用
arm-none-eabi-size工具查看各段大小,对比裁剪前后的 Flash 占用。
# CMakeLists.txt 示例
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -ffunction-sections -fdata-sections")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -ffunction-sections -fdata-sections")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --gc-sections")
# 链接脚本关键片段(.ld 文件)
.text :
{
KEEP(*(.isr_vector)) /* 中断向量表必须保留 */
*(.text*)
*(.rodata*)
} > FLASH
.data :
{
*(.data*)
} > RAM AT> FLASH
在 TFLite Micro 的 tensorflow/lite/micro 目录下,如果使用其自带的 Makefile 构建系统,可以通过 EXTRA_CFLAGS 和 EXTRA_LDFLAGS 变量传递这些选项:
make -f tensorflow/lite/micro/tools/make/Makefile \
TARGET=cortex_m_generic \
TARGET_ARCH=cortex-m4 \
EXTRA_CFLAGS="-ffunction-sections -fdata-sections" \
EXTRA_LDFLAGS="--gc-sections" \
generate_hello_world_project
4.3 案例拆解:TFLite Micro 语音识别模型裁剪实战
某智能家居项目使用 STM32F411(512KB Flash)运行 TFLite Micro 的语音唤醒词识别模型。初始固件编译后 Flash 占用为 186KB,其中模型数据占 42KB,TFLite Micro 框架代码占 98KB,应用层代码占 46KB。项目要求将总占用控制在 150KB 以内,以预留 OTA 升级空间。
初始现象:使用默认编译选项,未启用任何裁剪。通过 arm-none-eabi-size 查看,.text 段为 92KB,.rodata 段为 48KB,.data 段为 6KB。
分析过程:使用 arm-none-eabi-nm --size-sort 列出所有符号,发现大量未使用的调试函数和备用算子。例如,TFLite Micro 的 AllOpsResolver 默认注册了所有算子,但实际模型仅使用 5 种算子。此外,框架中的 DebugLog 相关函数在 release 版本中完全未被调用。
操作步骤:
- 在 Makefile 中添加
-ffunction-sections -fdata-sections --gc-sections,重新编译。 - 使用
arm-none-eabi-nm检查保留的符号,确认DebugLog系列函数已被移除。 - 进一步裁剪:将
AllOpsResolver替换为MicroMutableOpResolver,仅注册模型所需的 5 个算子。 - 对模型数据进行量化(从 float32 转为 int8),将模型数据从 42KB 压缩至 11KB。
结果对比:
| 优化阶段 | .text 段 | .rodata 段 | 总 Flash 占用 | 缩减比例 |
|---|---|---|---|---|
| 初始状态 | 92KB | 48KB | 186KB | — |
| 启用 gc-sections | 61KB | 35KB | 142KB | 23.7% |
| 替换 OpResolver | 48KB | 35KB | 129KB | 30.6% |
| 模型 int8 量化 | 48KB | 11KB | 105KB | 43.5% |
风险与代价:启用 --gc-sections 后,链接时间从 8 秒增加到 15 秒,但仍在可接受范围。主要风险在于中断向量表和启动代码可能被误删,因此必须在链接脚本中使用 KEEP() 指令保护。此外,如果某些函数通过函数指针间接调用,链接器可能无法识别引用关系,导致误删。在本案例中,MicroMutableOpResolver 的注册函数通过函数指针表调用,需要确保这些函数被显式保留。
风险提示:在 TFLite Micro 中,算子注册表(OpResolver)是典型的“间接引用”场景。如果使用 AllOpsResolver,所有算子都会被保留;如果改用 MicroMutableOpResolver,则必须确保注册的算子函数被 KEEP() 或显式引用,否则链接器可能将其误判为无用代码而删除。
4.4 常见误区与总结复盘
误区一:认为启用裁剪后代码一定变小。实际上,如果工程中所有函数都被引用,裁剪效果有限。裁剪的效果取决于代码中“死代码”的比例。对于 TFLite Micro 这类框架,由于支持多种算子,死代码比例通常较高,裁剪效果显著。
误区二:忽略链接脚本的适配。仅仅添加编译选项而不修改链接脚本,可能导致链接失败或段重叠。特别是当使用 KEEP() 指令时,需要明确哪些段必须保留。
误区三:混淆 -ffunction-sections 与 -fdata-sections 的作用范围。前者只影响函数,后者只影响全局变量。局部变量和栈变量不受影响。对于 TFLite Micro 中的大型常量数组(如权重数据),它们属于 .rodata 段,需要 -fdata-sections 才能实现独立裁剪。
误区四:认为裁剪后调试变得困难。虽然裁剪会移除未使用的调试函数,但保留的代码仍然可以通过 -g 选项生成调试信息。建议在开发阶段关闭裁剪,在发布阶段启用。
经验总结:在 TFLite Micro 工程中,建议按以下顺序进行裁剪优化:
- 先启用
-ffunction-sections -fdata-sections --gc-sections,这是成本最低的优化。 - 检查链接脚本,确保中断向量表、启动代码、异常处理函数被
KEEP()保护。 - 使用
arm-none-eabi-nm和size工具分析裁剪后的符号表,确认关键函数未被误删。 - 如果裁剪效果不理想,再考虑替换 OpResolver、量化模型等更深入的优化。
通过本章的编译器选项配置,TFLite Micro 固件的 Flash 占用通常可以降低 20% 至 40%。这一优化不改变任何业务逻辑,仅通过构建系统的调整即可实现,是嵌入式 AI 部署中性价比最高的优化手段之一。在实际项目中,建议将裁剪选项作为默认配置,同时保留一个未裁剪的调试构建版本,以便在开发阶段进行问题定位。