写代码的时候,很多人知道编译器会“自动优化”,但到底优化了没?优化了多少?光看程序跑得快了一点,并不能说明问题。想真正看到编译器在背后做了什么,得动手查一查。
从生成的汇编代码入手
最直接的办法,就是让编译器把生成的汇编代码输出出来,看看它到底干了啥。比如用 GCC 或 Clang 编译时,加上 -S 参数:
gcc -O2 -S test.c
这会生成一个 test.s 文件,里面是汇编代码。你可以对比开启和关闭优化(比如 -O0 和 -O2)时的差异。比如原本一个简单的循环求和:
int sum = 0;
for (int i = 0; i < 100; i++) {
sum += i;
}
在 -O2 下,可能直接被算成常数,汇编里连循环都没了,变成一条 mov 指令赋值 4950。这就是典型的常量折叠 + 循环展开。
利用编译器的诊断功能
Clang 提供了 -Rpass 系列选项,能告诉你哪些优化被成功应用了。例如:
clang -O2 -Rpass=loop-vectorize test.c
如果某个循环被向量化了,编译时就会输出类似:
test.c:5:3: remark: loop vectorized
反过来,用 -Rpass-missed 可以看到哪些本该优化却没成功的,帮助你调整代码结构。
看运行性能数据更实在
汇编看得懂是一回事,实际跑得快不快才是关键。写个简单计时程序,对比不同优化等级下的执行时间。比如处理一千万次浮点运算:
#include <time.h>
#include <stdio.h>
int main() {
clock_t start = clock();
double sum = 0.0;
for (int i = 0; i < 10000000; i++) {
sum += 1.0 / (i + 1);
}
clock_t end = clock();
printf("Time: %f ms\n", (double)(end - start) / CLOCKS_PER_SEC * 1000);
return 0;
}
分别用 -O0 和 -O3 编译,差距可能达到几倍。这时候你就明白,优化不是玄学,是实打实的指令减少和流水线利用。
别忘了内存和二进制大小
有时候优化会让生成的可执行文件变小。用 size 命令看看:
size a.out
你会发现 -O2 编译出来的程序,.text 段(代码段)可能比 -O0 还小——说明冗余指令被删了。反过来,某些向量化优化可能会增加代码体积,这是空间换时间的典型做法。
结合 perf 工具看底层表现
在 Linux 上,perf 能帮你看到程序运行时的 CPU 事件。比如:
perf stat ./a.out
可以看到指令数、缓存命中率、分支预测失败次数等。如果开启优化后,每条指令完成的工作更多(IPC 提高),那就说明优化生效了。比如原本每秒执行 1G 条指令,优化后只执行 600M 条就完成同样任务,这才是高效的体现。
编译器优化不是黑盒,只要愿意打开看看,谁都能搞清楚它到底做了什么。下次写完代码,不妨多加个 -S 或者跑个 perf,你会对“快”这个字,有全新的理解。