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 实战:一个典型的优化流程

我总结了一套流程,你可以直接套用:

  1. 第一步:先用 -Os 编译,查看固件大小
  2. 第二步:加上 -ffunction-sections -fdata-sections,重新编译
  3. 第三步:链接时加上 --gc-sections,再看大小
  4. 第四步:如果还不够,换成 -Oz,并加上 -fno-inline-functions
  5. 第五步:检查功能是否正常,特别是中断响应和时序

我记得有一次,一个项目 Flash 只剩 4KB 了。按照这个流程走下来,硬是挤出了 6KB 的空间。嗯,有时候优化就是这么神奇。

2.5 避坑指南

最后说几个我踩过的坑:

  • 不要盲目用 -Oz:有些库函数在 -Oz 下会变慢很多,比如 memcpy、memset。我建议只对关键模块用 -Oz,其他模块用 -Os
  • 检查优化后的代码:用 objdump -S 反汇编看看,确认优化没有引入 bug。我曾经遇到过 -Oz 把某个循环优化成了死循环。
  • 注意调试信息:优化等级越高,调试越困难。我习惯在调试阶段用 -O0,发布前再切到 -Os-Oz

好了,这一章的内容就这些。记住,编译器优化和链接脚本裁剪是相辅相成的。光优化不裁剪,效果减半;光裁剪不优化,也省不了多少。两手都要抓,两手都要硬。