2、编译器优化:GCC -Os/-Oz 优化等级详解与链接脚本裁剪
好,咱们直接进入正题。编译器优化,说白了就是让编译器帮你把代码“拧干”。在MCU这种资源受限的环境下,代码体积往往比运行速度更重要。我个人习惯,在项目初期就用 -Os 或 -Oz 来编译,而不是等到最后才想起来优化。
2.1 -Os 与 -Oz:到底差在哪?
先看这两个优化等级的核心区别:
| 优化等级 | 核心目标 | 典型行为 |
|---|---|---|
| -Os | 优化代码大小,同时兼顾性能 | 会展开一些小的内联函数,保留部分循环展开 |
| -Oz | 极致压缩代码大小,不惜牺牲性能 | 几乎不内联,禁用循环展开,甚至用更慢的指令序列来省空间 |
我在项目中遇到过这样的情况:用 -Os 编译,固件大小是 128KB,换成 -Oz 后降到了 112KB。省了 16KB,但某个中断响应时间从 3μs 变成了 5μs。嗯,这就是代价。
核心建议:如果你的MCU Flash 剩余空间在 10% 以上,用 -Os 就够了。如果 Flash 快满了,再考虑 -Oz。
2.2 实际编译选项怎么配?
以 GCC ARM 工具链为例,我常用的编译选项组合是这样的:
-Os -ffunction-sections -fdata-sections -fno-unroll-loops -fno-inline-functions
解释一下每个选项的作用:
- -Os:开启大小优化
- -ffunction-sections:每个函数放到独立的 section 中
- -fdata-sections:每个全局变量放到独立的 section 中
- -fno-unroll-loops:禁止循环展开,省空间
- -fno-inline-functions:禁止函数内联,省空间
你想想看,如果没有 -ffunction-sections,链接器只能以文件为单位裁剪。有了它,就能以函数为单位裁剪。这差别可大了去了。
小技巧:我习惯在 Makefile 里把优化等级定义成一个变量,比如 OPT_LEVEL ?= -Os,这样后期切换优化等级只需要改一个地方。
2.3 链接脚本裁剪:别让链接器“吃白饭”
光靠编译器优化还不够,链接脚本也得动手。链接脚本决定了哪些代码和数据最终会被放进固件里。
我曾经遇到一个坑:明明代码里没用某个函数,但固件里还是包含了它。查了半天,发现是链接脚本里把整个库都链接进来了。
正确的做法是使用 --gc-sections 选项:
# 在链接器选项中加入
-Wl,--gc-sections
这个选项会告诉链接器:把没有被引用的 section 都扔掉。配合前面的 -ffunction-sections,效果最好。
再看一个链接脚本的裁剪示例:
/* 原始链接脚本可能这样写 */
.text : {
*(.text)
}
/* 裁剪后的链接脚本 */
.text : {
*(.text*)
*(.text.*)
}
为什么要加 *?因为 -ffunction-sections 会把函数放到 .text.function_name 这样的 section 里。不加通配符,这些 section 就进不来了。
注意:使用 --gc-sections 时,要确保中断向量表、启动代码等关键部分没有被误删。我一般会在链接脚本里用 KEEP() 来保护它们:
KEEP(*(.isr_vector))
KEEP(*(.init))
KEEP(*(.fini))
2.4 实战:一个典型的优化流程
我总结了一套流程,你可以直接套用:
- 第一步:先用
-Os编译,查看固件大小 - 第二步:加上
-ffunction-sections -fdata-sections,重新编译 - 第三步:链接时加上
--gc-sections,再看大小 - 第四步:如果还不够,换成
-Oz,并加上-fno-inline-functions - 第五步:检查功能是否正常,特别是中断响应和时序
我记得有一次,一个项目 Flash 只剩 4KB 了。按照这个流程走下来,硬是挤出了 6KB 的空间。嗯,有时候优化就是这么神奇。
2.5 避坑指南
最后说几个我踩过的坑:
- 不要盲目用 -Oz:有些库函数在
-Oz下会变慢很多,比如 memcpy、memset。我建议只对关键模块用-Oz,其他模块用-Os。 - 检查优化后的代码:用
objdump -S反汇编看看,确认优化没有引入 bug。我曾经遇到过-Oz把某个循环优化成了死循环。 - 注意调试信息:优化等级越高,调试越困难。我习惯在调试阶段用
-O0,发布前再切到-Os或-Oz。
好了,这一章的内容就这些。记住,编译器优化和链接脚本裁剪是相辅相成的。光优化不裁剪,效果减半;光裁剪不优化,也省不了多少。两手都要抓,两手都要硬。