编译器优化如何查看实际效果

写代码的时候,很多人知道编译器会“自动”,但到底优化了没?优化了多少?光看程序跑得快了一点,并不能说明问题。想真正看到编译器在背后做了什么,得动手查一查。

从生成的汇编代码入手

最直接的办法,就是让编译器把生成的汇编代码输出出来,看看它到底干了啥。比如用 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,你会对“快”这个字,有全新的理解。