代码大小优化:-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 保持循环原样,节省了指令空间
第二招:函数节(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!
第三招:代码压缩与去除调试信息
调试信息是给开发者看的,不是给芯片跑的。发布版本里留着它,纯粹是浪费空间。
// 去除调试信息
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——里面全是调试符号。
另外,还有几个实用技巧:
- 字符串压缩:把日志字符串用索引表代替,运行时再查表展开
- 常量折叠:编译器会自动计算常量表达式,但你可以手动合并重复的常量
- 死代码消除:检查那些永远走不到的分支,删掉它们
知识体系总览
下面这张图总结了代码大小优化的核心路径:
实战:一个完整的优化案例
假设我们有一个简单的嵌入式程序,包含主逻辑和一堆调试函数。优化前后的对比:
| 优化步骤 | 代码体积 | 节省比例 |
|---|---|---|
| 原始版本(-O0,带调试信息) | 128 KB | — |
| 切换 -Os | 96 KB | 25% |
| 加上函数节 + gc-sections | 72 KB | 44% |
| 去除调试信息(strip) | 48 KB | 62% |
你看,从128KB到48KB,整整省了62%。这就是组合拳的威力。
核心原则:代码大小优化不是单一技巧,而是一套组合策略。从编译器到链接器,再到代码层面的清理,每一步都能挤出空间。
避坑指南
我曾经在优化一个蓝牙协议栈时,把 -Os 和 -ffunction-sections 都用上了,结果固件变小了,但蓝牙连接总是不稳定。查了两天才发现,--gc-sections 把某个中断服务程序里调用的一个辅助函数给剪掉了——因为那个函数只在中断上下文中被调用,链接器没检测到引用关系。
所以,我的建议是:
- 优化后一定要做功能回归测试,特别是中断和回调相关的代码
- 保留一份带调试信息的版本,方便后续排查问题
- 用
nm或objdump检查最终固件里有哪些符号,确认没有意外丢失
嗯,代码大小优化这事儿,说白了就是跟每一字节较劲。但当你看到固件从128KB瘦身到48KB,那种成就感,比写一百行业务代码都爽。