3、编译选项入门:-O0, -O1, -O2, -O3, -Os 的区别与选择

编译优化选项,说白了就是告诉编译器:「你想怎么折腾我的代码?」

我刚入行那会儿,觉得优化就是加个 -O2 完事。后来在项目里吃过亏,才明白每个选项背后都有代价。今天咱们就把 -O0 到 -Os 这几个老朋友聊透。

3.1 优化等级全景图

先看一张总图,理清这几个选项的关系:

GCC 优化等级全景图 -O0 -O1 -O2 -O3 -Os 各等级核心特征: -O0:无优化,调试友好,编译最快 -O1:基础优化,平衡编译速度与代码质量 -O2:标准优化,嵌入式项目首选 -O3:激进优化,追求极致速度 -Os:尺寸优化,Flash 紧张时的救星

3.2 逐个拆解:每个选项到底干了什么

-O0:无优化,调试模式

这是编译器的「老实人」模式。每条 C 语句都生成对应的机器码,变量老老实实存在内存里,循环不展开,函数不内联。

适用场景:开发调试阶段。我习惯在写新模块时先用 -O0,等逻辑跑通了再开优化。

关键点:-O0 下局部变量不会被优化掉,你可以用调试器看到每个变量的值。这是它最大的价值。

-O1:基础优化

打开了一些「安全」的优化开关。比如:

  • 死代码消除(删掉永远不会执行的代码)
  • 分支预测优化
  • 部分常量折叠

编译时间增加不多,但代码体积和速度都有改善。说实话,我很少单独用 -O1,要么 -O0 调试,要么直接上 -O2。

-O2:标准优化(嵌入式首选)

这是大多数嵌入式项目的默认选项。它打开了几乎所有不涉及「空间换时间」的优化:

  • 函数内联(小函数直接嵌入调用处)
  • 循环展开(减少循环控制开销)
  • 指令重排(充分利用 CPU 流水线)
  • 公共子表达式消除

我的建议:如果你的 Flash 和 RAM 都够用,直接上 -O2。我在 STM32、ESP32 项目里 90% 的情况都用它。

-O3:激进优化

在 -O2 基础上,打开了更多「可能增加代码体积」的优化:

  • 更激进的循环展开
  • 向量化(如果 CPU 支持 SIMD)
  • 函数重排(根据调用频率调整布局)

注意:代码体积可能膨胀 20%-50%。我曾在一次项目中用 -O3 把 64KB 的代码编译到了 96KB,Flash 直接爆了。

避坑指南:我曾经在 STM32F103 上用 -O3 编译一个定时器中断服务函数,结果编译器把中断函数内联到了主循环里,导致中断响应延迟暴增。后来查了两天才发现是优化等级的问题。

-Os:尺寸优化

这个选项的目标不是速度,而是代码体积。它在 -O2 的基础上,关闭了那些会让代码变大的优化:

  • 减少循环展开
  • 禁用函数内联(除非特别小)
  • 使用更紧凑的指令序列

3.3 各选项对比速查表

选项 编译速度 代码速度 代码体积 调试体验 典型场景
-O0 最快 最慢 最大 最好 开发调试
-O1 较快 中等 中等 较好 快速验证
-O2 中等 较快 较小 一般 生产发布(首选)
-O3 较慢 最快 可能膨胀 较差 计算密集型
-Os 中等 中等偏慢 最小 一般 Flash 受限设备

3.4 实际项目中的选择策略

光讲理论没意思,咱们看看真实项目里怎么选:

场景一:STM32 通用控制项目

Flash 256KB,RAM 64KB。这种配置比较充裕:

  • 调试阶段:-O0 + 调试符号
  • 发布版本:-O2 + -flto(链接时优化)

场景二:低端 MCU(如 STM8、ATtiny)

Flash 只有 8KB-32KB,寸土寸金:

  • 必须用 -Os
  • 必要时还要手动调整代码,把一些函数声明为 __attribute__((always_inline)) 或 __attribute__((noinline))

场景三:音频/视频处理

计算量大,对实时性要求高:

  • 核心算法模块用 -O3
  • 控制逻辑用 -O2
  • 可以按文件设置不同优化等级

实用技巧:在 Makefile 或 CMakeLists.txt 里,可以针对单个源文件设置优化等级:

# CMake 示例
set_source_files_properties(
  audio_algorithm.c
  PROPERTIES COMPILE_FLAGS "-O3"
)
set_source_files_properties(
  uart_driver.c
  PROPERTIES COMPILE_FLAGS "-O0"
)

3.5 一个真实的对比实验

拿一段简单的 FIR 滤波器代码,在 STM32F407 上实测:

优化等级 代码体积(字节) 执行时间(us) 编译时间(s)
-O0 4,320 12.8 0.8
-O1 2,880 6.4 1.2
-O2 2,560 4.2 1.5
-O3 3,840 3.8 2.1
-Os 2,240 5.6 1.4

看到了吗?-O2 在体积和速度之间取得了最好的平衡。-O3 虽然快了 10%,但体积膨胀了 50%。-Os 体积最小,但速度比 -O2 慢了 33%。

我的经验:别盲目追求 -O3。嵌入式系统里,代码体积往往比那 10% 的速度提升更重要。Flash 用完了,你连改 bug 的空间都没有。

3.6 调试与优化的平衡

你可能会问:「调试时用 -O0,发布时用 -O2,万一发布版本出 bug 了怎么办?」

嗯,这是个好问题。我的做法是:

  1. 开发阶段全程 -O0 + -g(调试符号)
  2. 功能稳定后,用 -O2 + -g 编译一版,跑一遍所有测试用例
  3. 如果测试通过,去掉 -g 生成最终发布版
  4. 如果测试失败,用 -O1 逐步排查,找到是哪个优化导致的

重要提醒:优化后的代码行为可能与源码不完全一致。比如:

  • volatile 变量被优化掉
  • 未初始化变量被赋予「意外」的值
  • 时序敏感的代码被重排

我曾经在调试一个 I2C 驱动时,-O0 下一切正常,-O2 下就卡死。最后发现是编译器把延时循环优化掉了——嗯,从那以后我学会了用 __asm__("nop") 来阻止优化。

3.7 小结

选优化等级没有银弹。我的建议是:

  • 默认用 -O2,这是最稳妥的选择
  • Flash 不够时切 -Os,别犹豫
  • 只有确定瓶颈在 CPU 时才考虑 -O3,而且要逐个文件测试
  • 调试永远用 -O0,别为了省那几秒编译时间把自己坑了

记住:编译器优化不是魔法,它只是按照规则帮你翻译代码。理解这些规则,你才能写出既高效又可靠的嵌入式程序。