编译器偷偷对你的代码做了什么?
一个可疑的优化
int x = 10;
int y = 20;
if (0) {
return x + y; // 这行会被编译吗?
}
return x;
把这段代码编译,你猜最终生成的机器码里有没有 x + y 的指令?
没有。 编译器的优化阶段会直接把它"扔掉"。
你以为的
编译器就是逐行翻译,把 C 代码机械地转成机器码。
实际情况:编译器优化
GCC 有多个优化级别(-O0 到 -O3 和 -Os),每个级别启用不同的优化策略。
| 级别 | 说明 | 编译速度 | 代码质量 |
|---|---|---|---|
-O0 | 无优化 | 最快 | 最差 |
-O1 | 基本优化 | 快 | 较好 |
-O2 | 大部分优化(推荐) | 较慢 | 好 |
-O3 | 激进的优化 | 慢 | 最好但代码体积大 |
-Os | 优化体积 | 慢 | 兼顾大小和速度 |
常见优化手段
1. 常量折叠(Constant Folding):
int x = 3 + 4 * 5; // 编译时直接算成 23
2. 死代码消除(Dead Code Elimination):
if (0) { ... } // 永远不会执行,直接删除
3. 常量传播(Constant Propagation):
int x = 42;
int y = x + 1; // 变成 int y = 43;
4. 内联展开(Inline Expansion):
int square(int x) { return x * x; }
int a = square(5); // 变成 int a = 5 * 5 = 25;
5. 公共子表达式消除(CSE):
int a = x * y * 2;
int b = x * y * 3; // x * y 只需算一次
动手验证
保存为 optimize.c:
#include <stdio.h>
int sum(int n) {
int total = 0;
for (int i = 0; i < n; i++) {
total += i;
}
return total;
}
int dead_code() {
int a = 10;
int b = 20;
if (0) {
return a + b; // 死代码,优化后会消失
}
return a;
}
int main() {
int result = 0;
// 循环展开的候选
for (int i = 0; i < 10; i++) {
result += sum(100);
}
// 常量传播的候选
int x = 10;
result += dead_code();
printf("result = %d\n", result);
return 0;
}
编译并对比汇编:
$ gcc -O0 -o optimize_O0 optimize.c
$ gcc -O2 -o optimize_O2 optimize.c
$ ./optimize_O0 && ./optimize_O2
命令解释:同一个源文件分别用 -O0(无优化)和 -O2(开启优化)编译。两个可执行文件输出结果应该相同,但内部实现完全不同。
运行结果(输出一致):
result = 49510
x = 10
用汇编对比:
$ gcc -O0 -S -o /tmp/O0.s optimize.c
$ gcc -O2 -S -o /tmp/O2.s optimize.c
$ wc -l /tmp/O0.s /tmp/O2.s
命令解释:-S 让 GCC 只编译到汇编就停止(不生成可执行文件)。wc -l 统计行数。
运行结果:
155 /tmp/O0.s
122 /tmp/O2.s
-O2 的汇编比 -O0 少了 33 行(约 21%)!
逐行看汇编差异
看 -O0 下的 sum 函数:
$ grep -A 30 '<sum>:' /tmp/O0.s
会看到真实的循环结构(有 cmp / jge / add 等指令)。
对比 -O2:
$ grep -A 10 '<sum>:' /tmp/O2.s
可能发现循环被展开甚至被完全消除(如果计算结果可以在编译时确定)。
更极端的例子
volatile int keep = 1; // volatile 阻止优化
int dead_code() {
int a = 10, b = 20;
if (0) return a + b; // 死代码
return a; // 优化后只有这一句
}
-O0下:dead_code有完整的函数体(即使if(0)内的代码依然存在)-O2下:dead_code被优化成return 10,if(0)完全消失
用图示理解编译器工作流程
源代码 (.c)
↓
预处理 (cpp) → gcc -E 展开宏、处理 #include
↓
编译 (cc1) → gcc -S 生成汇编
↓
汇编 (as) → gcc -c 生成目标文件 (.o)
↓
链接 (ld) → gcc 生成可执行文件
↓
优化贯穿以上全程
核心启示
- 编译器比你想象的聪明——你的代码可能被大幅改写,只要语义相同
- 不要在"不可能"的分支里放关键逻辑——编译器可能直接删除
volatile可以阻止优化——告诉编译器"这个变量可能被意想不到地修改"-O0适合调试,-O2适合发布——调试时用-O0,发布用-O2
下期预告
const int c = 42; 真的不能改吗?volatile int flag = 1; 到底阻止了什么优化?
下一期《const 和 volatile 在编译器眼里是什么?》,看编译器怎么对待这两个关键字,以及为什么硬件寄存器需要 volatile const。
更多内容详见专栏
6890

被折叠的 条评论
为什么被折叠?



