代码大小优化:-Os优化、代码压缩、函数节、去除调试信息

嵌入式开发的朋友们,咱们今天聊个实在话题——代码大小优化。

你可能觉得,现在存储芯片那么便宜,谁还在乎那几KB?我告诉你,我在做物联网终端项目时,一颗MCU的Flash就64KB,固件塞得满满当当,加个新功能都得抠字节。那时候我才真正明白,代码大小优化不是锦上添花,而是雪中送炭。

为什么要在意代码体积?

说白了,代码体积直接影响三件事:

  • 成本——小Flash的芯片便宜,批量生产省下的钱很可观
  • 启动速度——代码越小,从Flash加载到RAM的时间越短
  • 缓存命中率——紧凑的代码更容易塞进指令缓存,跑起来更快

你想想看,一个函数如果跨了两页Flash,取指时就要多等好几个周期。这就是为什么有时候优化体积反而能提升性能。

第一招:-Os 优化选项

GCC编译器提供了多个优化等级,其中 -Os 是专门为代码大小优化的。我个人的习惯是,在项目后期、功能稳定后,把编译选项从 -O2 切到 -Os,看看效果。

// 编译命令示例
gcc -Os -o firmware.elf main.c utils.c

-Os 做了什么?它启用了大部分 -O2 的优化,但会禁用那些可能让代码膨胀的技巧,比如循环展开。举个例子:

// 原始代码
for (int i = 0; i < 4; i++) {
    buffer[i] = data[i] * 2;
}

// -O2 可能展开成:
buffer[0] = data[0] * 2;
buffer[1] = data[1] * 2;
buffer[2] = data[2] * 2;
buffer[3] = data[3] * 2;

// -Os 保持循环原样,节省了指令空间
我的经验:在ARM Cortex-M系列上,-Os 通常能比 -O2 减少15%-30%的代码体积。但要注意,有些场景下 -Os 生成的代码执行速度会慢一些,需要权衡。

第二招:函数节(Function Sections)

这个技巧很多人不知道。默认情况下,链接器是以目标文件(.o)为单位进行垃圾回收的。也就是说,只要这个文件里有一个函数被调用了,整个文件的所有函数都会被链接进来。

解决办法是使用 -ffunction-sections-Wl,--gc-sections

// 编译时
gcc -Os -ffunction-sections -c main.c -o main.o
gcc -Os -ffunction-sections -c utils.c -o utils.o

// 链接时
gcc -Wl,--gc-sections -o firmware.elf main.o utils.o

-ffunction-sections 把每个函数放到独立的section里,--gc-sections 则把没被引用的section统统扔掉。

我曾经接手过一个项目,代码里有一大堆调试用的测试函数,上线时根本用不到。加上这两个选项后,固件直接从128KB降到了96KB——整整省了32KB!

注意:函数节会增加目标文件的数量,编译时间会稍微变长。另外,如果你用了内联汇编或者某些特殊的section属性,要小心别把需要的代码给剪掉了。

第三招:代码压缩与去除调试信息

调试信息是给开发者看的,不是给芯片跑的。发布版本里留着它,纯粹是浪费空间。

// 去除调试信息
gcc -Os -s -o firmware_release.elf firmware_debug.elf

// 或者编译时直接不带调试信息
gcc -Os -o firmware.elf main.c utils.c   // 不加 -g 选项

-s 选项会 strip 掉符号表和调试信息。我见过有些团队发布固件时忘了 strip,结果一个本该 64KB 的固件硬是变成了 200KB——里面全是调试符号。

另外,还有几个实用技巧:

  • 字符串压缩:把日志字符串用索引表代替,运行时再查表展开
  • 常量折叠:编译器会自动计算常量表达式,但你可以手动合并重复的常量
  • 死代码消除:检查那些永远走不到的分支,删掉它们

知识体系总览

下面这张图总结了代码大小优化的核心路径:

代码大小优化知识体系 目标:最小化代码体积 编译器优化 链接器优化 代码清理 -Os 优化等级 禁用循环展开 常量折叠与传播 -ffunction-sections --gc-sections 垃圾回收 函数级裁剪 去除调试信息 (-s) 字符串压缩 死代码消除 更小的固件 · 更低的成本

实战:一个完整的优化案例

假设我们有一个简单的嵌入式程序,包含主逻辑和一堆调试函数。优化前后的对比:

优化步骤 代码体积 节省比例
原始版本(-O0,带调试信息) 128 KB
切换 -Os 96 KB 25%
加上函数节 + gc-sections 72 KB 44%
去除调试信息(strip) 48 KB 62%

你看,从128KB到48KB,整整省了62%。这就是组合拳的威力。

核心原则:代码大小优化不是单一技巧,而是一套组合策略。从编译器到链接器,再到代码层面的清理,每一步都能挤出空间。

避坑指南

我曾经在优化一个蓝牙协议栈时,把 -Os-ffunction-sections 都用上了,结果固件变小了,但蓝牙连接总是不稳定。查了两天才发现,--gc-sections 把某个中断服务程序里调用的一个辅助函数给剪掉了——因为那个函数只在中断上下文中被调用,链接器没检测到引用关系。

所以,我的建议是:

  • 优化后一定要做功能回归测试,特别是中断和回调相关的代码
  • 保留一份带调试信息的版本,方便后续排查问题
  • nmobjdump 检查最终固件里有哪些符号,确认没有意外丢失

嗯,代码大小优化这事儿,说白了就是跟每一字节较劲。但当你看到固件从128KB瘦身到48KB,那种成就感,比写一百行业务代码都爽。