强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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-conversionif 转换优化
-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-jumpsCSE 跟踪跳转
-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-conversionswitch 转换
-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-vectorizeSLP 向量化
-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 文件

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