GCC 完全指南 / 06 - 优化技术
06 - 优化技术
深入理解 GCC 的各级优化技术——从 -O0 到 -O3、-Ofast、LTO、PGO,掌握性能调优的核心方法。
6.1 优化级别详解
各优化级别对比
| 级别 |
选项 |
编译速度 |
运行速度 |
代码大小 |
调试友好 |
适用场景 |
| 无优化 |
-O0 |
最快 |
最慢 |
最大 |
最好 |
开发调试 |
| 调试优化 |
-Og |
快 |
较快 |
较大 |
好 |
日常开发 |
| 基本优化 |
-O1 |
较快 |
较快 |
较小 |
较好 |
一般构建 |
| 推荐优化 |
-O2 |
中等 |
快 |
小 |
一般 |
生产构建 |
| 激进优化 |
-O3 |
较慢 |
最快 |
较大 |
较差 |
性能关键 |
| 大小优化 |
-Os |
中等 |
较快 |
最小 |
一般 |
嵌入式/容器 |
| 极限优化 |
-Ofast |
最慢 |
可能最快 |
大 |
差 |
科学计算 |
实际编译对比
# 创建测试文件
cat > bench.c << 'EOF'
#include <stdio.h>
#include <time.h>
#define N 100000000
double compute(void) {
double sum = 0.0;
for (int i = 0; i < N; i++) {
sum += 1.0 / (i + 1);
}
return sum;
}
int main(void) {
clock_t start = clock();
double result = compute();
clock_t end = clock();
printf("Result: %.6f\n", result);
printf("Time: %.3f sec\n", (double)(end - start) / CLOCKS_PER_SEC);
return 0;
}
EOF
# 对比不同优化级别
for opt in O0 Og O1 O2 O3 Os Ofast; do
gcc -$opt -o bench_$opt bench.c
echo -n "$opt: "
./bench_$opt
done
6.2 各优化级别的具体优化项
-O1 包含的优化
# 查看 -O1 包含的具体优化
gcc -O1 -Q --help=optimizers 2>&1 | grep enabled
| 优化项 |
说明 |
-fauto-inc-dec |
自增/自减优化 |
-fbranch-count-reg |
分支计数寄存器 |
-fcombine-stack-adjustments |
合并栈调整 |
-fcompare-elim |
消除比较操作 |
-fcprop-registers |
寄存器传播 |
-fdce |
死代码消除 |
-fdefer-pop |
延迟弹栈 |
-fdelayed-branch |
延迟分支 |
-fdse |
死存储消除 |
-fguess-branch-probability |
分支概率预测 |
-fif-conversion |
if 转换优化 |
-finline-functions-called-once |
内联只调用一次的函数 |
-fipa-modref |
过程间修改/引用分析 |
-fipa-profile |
过程间性能分析 |
-fipa-pure-const |
过程间纯函数/常量分析 |
-fmerge-constants |
合并相同常量 |
-fmove-loop-invariants |
循环不变量外提 |
-fomit-frame-pointer |
省略帧指针 |
-freorder-blocks |
基本块重排序 |
-fshrink-wrap |
函数入口延迟保存寄存器 |
-fsplit-wide-types |
分裂宽类型 |
-ftree-ccp |
条件常量传播 |
-ftree-ch |
循环变换 |
-ftree-coalesce-vars |
变量合并 |
-ftree-dce |
死代码消除(Tree 级) |
-ftree-dominator-opts |
支配树优化 |
-ftree-fre |
前向冗余消除 |
-ftree-sra |
标量替换聚合体 |
-ftree-ter |
临时表达式替换 |
-O2 在 -O1 基础上增加的优化
| 优化项 |
说明 |
-falign-functions |
函数对齐 |
-falign-jumps |
跳转目标对齐 |
-falign-loops |
循环入口对齐 |
-fcaller-saves |
调用者保存寄存器 |
-fcode-hoisting |
代码提升 |
-fcrossjumping |
交叉跳转 |
-fcse-follow-jumps |
CSE 跟踪跳转 |
-fdelete-null-pointer-checks |
删除空指针检查 |
-fdevirtualize |
去虚拟化 |
-fdevirtualize-speculatively |
推测去虚拟化 |
-fexpensive-optimizations |
昂贵优化集合 |
-fforward-propagate |
前向传播 |
-fgcse |
全局公共子表达式消除 |
-fhoist-adjacent-loads |
提升相邻加载 |
-finline-functions |
内联适合的函数 |
-finline-small-functions |
内联小函数 |
-fipa-bit-cp |
过程间位传播 |
-fipa-cp |
过程间常量传播 |
-fipa-icf |
过程间相同函数折叠 |
-fipa-ra |
过程间寄存器分配 |
-fipa-sra |
过程间标量替换 |
-fisolate-erroneous-paths |
隔离错误路径 |
-flra-remat |
局部寄存器分配重载 |
-fmove-loop-stores |
循环存储移动 |
-foptimize-sibling-calls |
尾调用优化 |
-foptimize-strlen |
字符串操作优化 |
-fpartial-inlining |
部分内联 |
-fpeephole2 |
窥孔优化 |
-freorder-blocks-algorithm=stc |
基本块重排序算法 |
-freorder-functions |
函数重排序 |
-frerun-cse-after-loop |
循环后重新 CSE |
-fschedule-insns |
指令调度 |
-fschedule-insns2 |
指令调度(第二遍) |
-fstore-merging |
存储合并 |
-fstrict-aliasing |
严格别名分析 |
-fthread-jumps |
线程跳转 |
-ftree-builtin-call-dce |
内置函数死调用消除 |
-ftree-pre |
部分冗余消除 |
-ftree-switch-conversion |
switch 转换 |
-ftree-tail-merge |
尾部合并 |
-ftree-vrp |
值范围传播 |
-O3 在 -O2 基础上增加的优化
| 优化项 |
说明 |
-fgcse-after-reload |
重载后 GCSE |
-finline-functions |
更激进的函数内联 |
-fipa-cp-clone |
过程间常量传播克隆 |
-floop-interchange |
循环交换 |
-floop-unroll-and-jam |
循环展开并合并 |
-fpeel-loops |
循环剥离 |
-fpredictive-commoning |
预测公共化 |
-fsplit-loops |
循环分裂 |
-fsplit-paths |
路径分裂 |
-ftree-loop-distribute-patterns |
循环模式分布 |
-ftree-loop-distribution |
循环分布 |
-ftree-loop-vectorize |
循环自动向量化 |
-ftree-partial-pre |
部分 PRE |
-ftree-slp-vectorize |
SLP 向量化 |
-funswitch-loops |
循环外提不变条件 |
-fvect-cost-model |
向量化代价模型 |
-fvect-cost-model=dynamic |
动态向量化代价模型 |
6.3 特殊优化选项
-Os(大小优化)
# -Os 开启大部分 -O2 的优化,但禁用增加代码大小的优化
gcc -Os -o hello_small main.c
# 具体差异:
# - 禁用函数对齐(-falign-functions=0)
# - 更保守的内联阈值
# - 禁用循环展开
# - 优先选择更小的指令序列
-Og(调试友好优化)
# -Og 启用不影响调试的优化
gcc -g -Og -o hello main.c
# 包含的优化:
# -fauto-inc-dec
# -fdefer-pop
# -fdse
# -fif-conversion
# -fmerge-constants
# 不包含(以免影响调试):
# - 帧指针省略(保持栈帧可追溯)
# - 激进的内联(保持函数边界清晰)
# - 指令重排序(保持执行顺序与源码一致)
-Ofast(极限优化)
# -Ofast = -O3 + 可能违反标准的优化
gcc -Ofast -o hello_fast main.c
# 包含的关键非标准优化:
# -ffast-math ← 允许浮点运算不符合 IEEE 754
# -fallow-store-data-races ← 允许存储数据竞争
-ffast-math 的具体影响
| 标志 |
影响 |
-fno-math-errno |
数学函数不设置 errno |
-funsafe-math-optimizations |
允许不安全的浮点优化 |
-ffinite-math-only |
假设没有 NaN 和 Inf |
-fno-rounding-math |
假设默认舍入模式 |
-fno-signaling-nans |
假设没有 signaling NaN |
-fcx-limited-range |
复数运算使用有限范围 |
-fexcess-precision=fast |
允许使用更高精度的中间结果 |
// -ffast-math 可能导致的问题:
double x = 0.0 / 0.0; // NaN
if (x != x) { // NaN 的标准检测方式
printf("NaN detected\n");
}
// 使用 -ffast-math 时,-ffinite-math-only 使此检查失效!
// 如果你的代码依赖 NaN/Inf 行为,不要使用 -Ofast
6.4 优化级别的具体对比实验
自动向量化
cat > vector_test.c << 'EOF'
#include <stdio.h>
#include <stdlib.h>
#define N 10000
void add_arrays(float *a, float *b, float *c, int n) {
for (int i = 0; i < n; i++) {
c[i] = a[i] + b[i];
}
}
int main(void) {
float a[N], b[N], c[N];
for (int i = 0; i < N; i++) {
a[i] = (float)i;
b[i] = (float)(i * 2);
}
add_arrays(a, b, c, N);
printf("c[0]=%.1f c[N-1]=%.1f\n", c[0], c[N-1]);
return 0;
}
EOF
# 无向量化
gcc -O2 -fopt-info-vec-missed -o vec_test vector_test.c
# 输出类似: vector_test.c:6:5: missed: couldn't vectorize loop
# 有向量化
gcc -O2 -fopt-info-vec-optimized -o vec_test vector_test.c
# 输出类似: vector_test.c:6:5: optimized: loop vectorized
# 查看向量化报告
gcc -O3 -ftree-vectorizer-verbose=2 -o vec_test vector_test.c 2>&1
内联函数优化
cat > inline_test.c << 'EOF'
static int square(int x) { return x * x; }
static int cube(int x) { return x * x * x; }
int compute(int n) {
int sum = 0;
for (int i = 0; i < n; i++) {
sum += square(i) + cube(i);
}
return sum;
}
EOF
# 查看是否内联
gcc -O2 -Winline -o inline_test inline_test.c
# 查看内联决策
gcc -O2 -fdump-ipa-inline-details -o inline_test inline_test.c
# 生成 inline_test.c.***.ipa-inline 文件
6.5 LTO(Link-Time Optimization)
LTO 在链接阶段进行全程序优化,可以跨编译单元进行内联、常量传播等优化。
# 启用 LTO
gcc -flto -O2 -o hello main.c greet.c
# 等价于分步 LTO
gcc -flto -O2 -c main.c -o main.o
gcc -flto -O2 -c greet.c -o greet.o
gcc -flto -O2 -o hello main.o greet.o
LTO 的优势
| 优势 |
说明 |
| 跨文件内联 |
main.c 可以内联 greet.c 中的函数 |
| 跨文件常量传播 |
链接时确定跨文件的常量值 |
| 跨文件死代码消除 |
删除整个程序中未使用的代码 |
| 全程序类型分析 |
C++ 去虚拟化更有效 |
| 优化代码大小 |
去除冗余的模板实例化(C++) |
LTO 的类型
# 默认 LTO(使用完整的 GIMPLE IR,最优化但最慢)
gcc -flto -O2 -o hello main.c
# 薄 LTO(ThinLTO,GCC 11+,更快的编译速度)
gcc -flto=auto -O2 -o hello main.c
# flto=auto 让 GCC 自动决定使用完整 LTO 还是并行 LTO
# 指定并行 LTO 线程数
gcc -flto=4 -O2 -o hello main.c # 使用 4 个线程
LTO 的实际效果
# 对比 LTO 开启前后的代码大小和性能
cat > lto_test_a.c << 'EOF'
static int internal_func(int x) {
return x * x + 2 * x + 1;
}
int compute_a(int n) {
int sum = 0;
for (int i = 0; i < n; i++) {
sum += internal_func(i);
}
return sum;
}
EOF
cat > lto_test_b.c << 'EOF'
extern int compute_a(int n);
#include <stdio.h>
int main(void) {
printf("%d\n", compute_a(1000));
return 0;
}
EOF
# 无 LTO
gcc -O2 -c lto_test_a.c -o lto_test_a.o
gcc -O2 -c lto_test_b.c -o lto_test_b.o
gcc -o no_lto lto_test_a.o lto_test_b.o
# 有 LTO
gcc -flto -O2 -c lto_test_a.c -o lto_test_a_lto.o
gcc -flto -O2 -c lto_test_b.c -o lto_test_b_lto.o
gcc -flto -O2 -o with_lto lto_test_a_lto.o lto_test_b_lto.o
# 对比
ls -l no_lto with_lto
# with_lto 通常更小,因为 LTO 可以优化掉 internal_func
LTO 的注意事项
| 注意事项 |
说明 |
| 编译时间 |
LTO 显著增加链接时间 |
| 内存消耗 |
链接时需要加载所有 GIMPLE IR,内存占用高 |
| 调试信息 |
LTO 可能影响调试信息的准确性 |
| 兼容性 |
LTO 编译的 .o 文件不能与非 LTO 的混用 |
| 静态库 |
LTO 与静态库一起使用时需要 gcc-ar / gcc-ranlib |
# 使用 LTO 兼容的 ar 和 ranlib
gcc -flto -O2 -c lib.c -o lib.o
gcc-ar rcs libmylib.a lib.o # 使用 gcc-ar 而非 ar
gcc-ranlib libmylib.a # 使用 gcc-ranlib
6.6 PGO(Profile-Guided Optimization)
PGO 使用实际运行时数据来指导编译器优化,通常比单纯 LTO 更有效。
PGO 工作流程
步骤 1: 插桩编译
源代码 → gcc -fprofile-generate → 插桩的可执行文件
步骤 2: 收集运行数据
插桩的可执行文件 → 运行典型负载 → .gcda 文件(性能数据)
步骤 3: 优化编译
源代码 + .gcda → gcc -fprofile-use → 优化后的可执行文件
PGO 实际操作
cat > pgo_test.c << 'EOF'
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
// 模拟实际的热点函数
int process_data(const char *data, int len) {
int sum = 0;
for (int i = 0; i < len; i++) {
sum += data[i] * (i % 7 + 1);
}
return sum;
}
int classify(int value) {
if (value > 1000) return 3;
if (value > 100) return 2;
if (value > 10) return 1;
return 0;
}
int main(void) {
srand(time(NULL));
char data[1024];
for (int i = 0; i < sizeof(data); i++) {
data[i] = rand() % 256;
}
int results[4] = {0};
for (int i = 0; i < 100000; i++) {
int val = process_data(data, sizeof(data));
results[classify(val)]++;
}
for (int i = 0; i < 4; i++) {
printf("Class %d: %d\n", i, results[i]);
}
return 0;
}
EOF
# 步骤 1: 插桩编译
gcc -O2 -fprofile-generate -o pgo_instrumented pgo_test.c
# 步骤 2: 运行收集数据
./pgo_instrumented
# 生成 pgo_test.gcda 文件
# 步骤 3: 使用数据优化编译
gcc -O2 -fprofile-use -o pgo_optimized pgo_test.c
# 清理
rm -f pgo_instrumented pgo_test.gcda pgo_test.gcno
PGO + LTO 组合
# 组合使用效果最佳
# 步骤 1: 插桩编译(LTO + PGO generate)
gcc -flto -O2 -fprofile-generate -o pgo_instrumented main.c
# 步骤 2: 运行
./pgo_instrumented
# 步骤 3: 优化编译(LTO + PGO use)
gcc -flto -O2 -fprofile-use -o final_optimized main.c
PGO 的效果
| 优化效果 |
说明 |
| 分支预测 |
编译器知道哪些分支更常被走,优化代码布局 |
| 函数内联 |
热点函数更可能被内联 |
| 基本块排序 |
热路径放在连续内存中,提高缓存命中率 |
| 循环优化 |
知道循环迭代次数分布,优化循环策略 |
6.7 特定函数优化
// 对特定函数设置优化级别
__attribute__((optimize("O3")))
void hot_function(int *data, int n) {
for (int i = 0; i < n; i++) {
data[i] *= 2;
}
}
__attribute__((optimize("O0")))
void debug_function(void) {
// 不优化,便于调试
}
// 对特定函数禁用某项优化
__attribute__((optimize("no-tree-vectorize")))
void no_vectorize_func(int *data, int n) {
// 禁止此函数的自动向量化
}
使用 Pragma 控制
#pragma GCC optimize("O3")
void hot_function(void) { ... }
#pragma GCC reset_options
#pragma GCC target("avx2")
void avx2_function(float *data, int n) {
// 此函数使用 AVX2 指令集
}
6.8 优化注意事项
严格别名规则(Strict Aliasing)
// -fstrict-aliasing(-O2 及以上默认开启)
// 通过不同类型指针访问同一内存是未定义行为!
int x = 0x3f800000;
float *f = (float *)&x; // ❌ 严格别名违规
float val = *f; // 未定义行为!
// 正确做法:使用 memcpy
int x = 0x3f800000;
float f;
memcpy(&f, &x, sizeof(f)); // ✅ 安全且编译器能优化掉
// 或使用 union(GCC 支持但标准未保证)
union { int i; float f; } u;
u.i = 0x3f800000;
float val = u.f; // GCC 支持
-ffast-math 的陷阱
// IEEE 754 保证的 NaN 检测在 -ffast-math 下失效
int is_nan(double x) {
return x != x; // 在 -ffast-math 下始终返回 false!
}
// 安全替代方案
#include <math.h>
int is_nan_safe(double x) {
return isnan(x); // 使用标准库函数
}
要点回顾
| 要点 |
核心内容 |
| -O0 |
默认,无优化,调试最准确 |
| -Og |
日常开发推荐,兼顾调试和性能 |
| -O2 |
生产构建推荐,全面安全的优化 |
| -O3 |
激进优化,自动向量化,代码可能更大 |
| -Ofast |
包含 -ffast-math,浮点行为可能违反标准 |
| LTO |
链接时优化,跨文件内联和死代码消除 |
| PGO |
基于运行数据的优化,通常比 LTO 效果更好 |
| 严格别名 |
-O2 开启,注意类型双关问题 |
注意事项
不要盲目使用 -O3: 某些场景下 -O3 可能因为激进的循环优化导致代码变大变慢。建议先用 -O2 基准,再尝试 -O3 对比。
-Ofast 的浮点问题: 依赖 IEEE 754 特定行为(NaN、Inf、舍入模式)的代码不要使用 -Ofast。
LTO 的调试困难: LTO 优化后,调试信息可能不准确,部分变量被优化掉。调试时建议关闭 LTO。
PGO 的负载代表性: PGO 的效果取决于收集数据时的负载是否能代表实际使用场景。使用不具代表性的负载可能导致 PGO 反而降低性能。
扩展阅读
下一步
→ 07 - 调试支持:学习如何使用 -g 选项生成调试信息,与 GDB 集成进行高效调试。