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 集成进行高效调试。