软件研发 --- 应知应会 之 编译器优化
一、概述
编译器优化是指编译器在将源代码转换为机器代码的过程中,自动改进代码的效率,使生成的程序运行更快、占用内存更少,同时保持程序语义不变。
text
源代码 → [前端分析] → IR中间表示 → [优化Pass] → [后端] → 机器码
↑
优化发生在这里
二、优化的层次
text
┌─────────────────────────────────────┐
│ 高层优化(源码级别) │ 算法级别
├─────────────────────────────────────┤
│ 中间表示优化(IR级别) │ 编译器主要工作
├─────────────────────────────────────┤
│ 低层优化(机器码级别) │ 指令选择/寄存器分配
└─────────────────────────────────────┘
三、常见优化技术详解
3.1 常量折叠(Constant Folding)
在编译期计算常量表达式,避免运行时计算。
C
// 优化前
int x = 2 * 3 + 4;
int y = sizeof(int) * 8;
// 优化后(编译器直接替换)
int x = 10;
int y = 32;
C
// 更复杂的例子
if (1 == 1) { // 编译器知道这永远为真
do_something();
}
// 优化后直接变成:
do_something();
3.2 常量传播(Constant Propagation)
追踪变量的常量值,并将变量替换为常量。
C
// 优化前
int a = 5;
int b = a + 3; // a是常量5
int c = b * 2; // b是常量8
// 优化后
int a = 5;
int b = 8; // 5 + 3 = 8
int c = 16; // 8 * 2 = 16
3.3 死代码消除(Dead Code Elimination)
删除永远不会执行或结果不会被使用的代码。
C
// 优化前
int foo() {
int x = compute(); // x被计算但从未使用
int y = 42;
return y;
int z = 100; // return后的代码不可达
}
// 优化后
int foo() {
return 42;
}
C
// 条件死代码
#define DEBUG 0
if (DEBUG) {
printf("debug info"); // 永远不执行,直接删除
}
3.4 公共子表达式消除(CSE - Common Subexpression Elimination)
避免重复计算相同的表达式。
C
// 优化前
float a = b*c + g;
float d = b*c * e; // b*c 被计算了两次
// 优化后
float tmp = b*c; // 只计算一次
float a = tmp + g;
float d = tmp * e;
C
// 数组访问中的CSE
a[i*width + j] = a[i*width + j] + 1;
// 优化后
int idx = i*width + j; // 索引只计算一次
a[idx] = a[idx] + 1;
3.5 循环优化
3.5.1 循环不变量外提(Loop Invariant Code Motion)
将循环体内不随迭代改变的计算移到循环外。
C
// 优化前
for (int i = 0; i < n; i++) {
a[i] = b[i] * sqrt(x); // sqrt(x) 每次循环都计算
}
// 优化后
double tmp = sqrt(x); // 移到循环外,只计算一次
for (int i = 0; i < n; i++) {
a[i] = b[i] * tmp;
}
3.5.2 循环展开(Loop Unrolling)
减少循环控制开销,增加指令级并行。
C
// 优化前
for (int i = 0; i < 100; i++) {
a[i] = b[i] + c[i];
}
// 展开4次后
for (int i = 0; i < 100; i += 4) {
a[i] = b[i] + c[i];
a[i+1] = b[i+1] + c[i+1];
a[i+2] = b[i+2] + c[i+2];
a[i+3] = b[i+3] + c[i+3];
}
text
展开前:每次迭代 = 计算 + 增量i + 比较 + 跳转
展开后:每4次计算只需要 1次增量 + 1次比较 + 1次跳转
3.5.3 循环融合(Loop Fusion)
将多个循环合并,提高缓存利用率。
C
// 优化前:两次遍历数组
for (int i = 0; i < n; i++) a[i] = b[i] + 1;
for (int i = 0; i < n; i++) c[i] = a[i] * 2;
// 优化后:一次遍历
for (int i = 0; i < n; i++) {
a[i] = b[i] + 1;
c[i] = a[i] * 2; // 更好的缓存局部性
}
3.5.4 循环分裂(Loop Fission/Distribution)
将一个循环拆分,提高向量化可能性。
C
// 优化前(难以向量化,因为有依赖)
for (int i = 0; i < n; i++) {
a[i] = a[i-1] + b[i]; // 存在依赖
c[i] = d[i] * e[i]; // 无依赖,可向量化
}
// 优化后
for (int i = 0; i < n; i++)
a[i] = a[i-1] + b[i]; // 保持顺序
for (int i = 0; i < n; i++)
c[i] = d[i] * e[i]; // 可以SIMD向量化
3.5.5 循环交换(Loop Interchange)
改变嵌套循环顺序,提高内存访问局部性。
C
// 优化前(按列访问,缓存不友好)
for (int j = 0; j < N; j++)
for (int i = 0; i < N; i++)
sum += A[i][j]; // 跨行访问
// 优化后(按行访问,缓存友好)
for (int i = 0; i < N; i++)
for (int j = 0; j < N; j++)
sum += A[i][j]; // 连续内存访问
text
内存布局(行主序):
A[0][0] A[0][1] A[0][2] | A[1][0] A[1][1] ...
←── 缓存行 ──→
按行访问:顺序读取,缓存命中率高 ✓
按列访问:跳跃读取,频繁缓存缺失 ✗
3.6 内联展开(Inlining)
将函数调用替换为函数体,消除调用开销。
C
// 优化前
inline int add(int a, int b) {
return a + b;
}
int result = add(x, y);
// 内联后(编译器直接替换)
int result = x + y; // 无函数调用开销
函数调用的开销:
text
调用函数需要:
1. 保存寄存器
2. 设置参数(压栈或寄存器传递)
3. 跳转到函数地址
4. 建立栈帧
5. 执行函数体
6. 恢复现场
7. 返回
内联后:只有函数体执行
3.7 强度削减(Strength Reduction)
用代价更低的操作替换代价高的操作。
C
// 乘法 → 加法
// 优化前
for (int i = 0; i < n; i++) {
a[i*8] = ...; // 每次乘法
}
// 优化后
for (int i = 0, j = 0; i < n; i++, j += 8) {
a[j] = ...; // 改为加法,代价更低
}
C
// 乘以2的幂 → 移位
x * 2 → x << 1
x * 4 → x << 2
x / 8 → x >> 3
x % 16 → x & 15
C
// 除法 → 乘以倒数(浮点)
x / 3.0 → x * 0.333... // 乘法比除法快
3.8 尾递归优化(Tail Call Optimization)
将尾递归转换为循环,避免栈溢出。
C
// 优化前(尾递归)
int factorial(int n, int acc) {
if (n == 0) return acc;
return factorial(n-1, n*acc); // 尾调用
}
// 优化后(编译器转为循环)
int factorial(int n, int acc) {
while (n != 0) {
acc = n * acc;
n = n - 1;
}
return acc;
}
3.9 向量化(Vectorization / SIMD)
利用SIMD指令同时处理多个数据。
C
// 标量代码
for (int i = 0; i < 8; i++) {
c[i] = a[i] + b[i];
}
// 向量化后(概念示意)
// 一条SIMD指令同时处理8个float
__m256 va = _mm256_load_ps(a); // 加载8个float
__m256 vb = _mm256_load_ps(b); // 加载8个float
__m256 vc = _mm256_add_ps(va, vb); // 8个加法同时执行
_mm256_store_ps(c, vc);
text
标量: a[0]+b[0], a[1]+b[1], ..., a[7]+b[7] → 8次操作
SIMD: [a0,a1,a2,a3,a4,a5,a6,a7]
+ [b0,b1,b2,b3,b4,b5,b6,b7]
= [c0,c1,c2,c3,c4,c5,c6,c7] → 1次操作
3.10 别名分析(Alias Analysis)
确定指针是否指向相同内存,从而解锁更多优化。
C
// 编译器不确定a和b是否重叠,难以优化
void add(float* a, float* b, float* c, int n) {
for (int i = 0; i < n; i++)
c[i] = a[i] + b[i];
}
// 使用restrict告诉编译器指针不重叠
void add(float* restrict a,
float* restrict b,
float* restrict c, int n) {
for (int i = 0; i < n; i++)
c[i] = a[i] + b[i]; // 现在可以安全向量化
}
四、GCC/Clang 优化等级
text
优化等级 说明 典型用途
─────────────────────────────────────────────────
-O0 无优化(默认) 调试
-O1 基本优化,不增加编译时间
-O2 推荐优化,不做空间换时间 生产环境 ✓
-O3 激进优化(向量化/内联等) 高性能计算
-Os 优化代码大小 嵌入式系统
-Oz 最小化代码大小(Clang)
-Og 调试友好的优化 开发调试
Bash
# 查看优化后的汇编
gcc -O2 -S -o output.s input.c
# 查看优化报告
gcc -O2 -fopt-info-vec program.c # 向量化信息
gcc -O2 -fopt-info-all program.c # 所有优化信息
五、优化的局限性
5.1 可观察副作用限制优化
C
// 编译器不能消除这个"死代码"
// 因为volatile表示可观察副作用
volatile int* hw_reg = (volatile int*)0x1000;
*hw_reg = 0; // 不能被优化掉!
5.2 指针别名阻止优化
C
void copy(int* dst, int* src, int n) {
for (int i = 0; i < n; i++)
dst[i] = src[i];
// 如果dst和src重叠,向量化可能产生错误结果
// 编译器必须保守处理
}
5.3 函数调用阻止优化
C
// 编译器不知道外部函数的副作用
for (int i = 0; i < n; i++) {
result += external_function(i); // 无法优化循环
}
六、优化可视化工具
Bash
# 1. Compiler Explorer(godbolt.org)
# 在线查看不同优化等级的汇编输出
# 2. 查看优化passes(LLVM)
clang -O2 -mllvm -print-after-all program.c
# 3. 生成优化报告
g++ -O2 -fopt-info program.cpp
# 4. 查看向量化
g++ -O3 -fopt-info-vec-optimized program.cpp
七、总结
text
优化类型 收益来源 适用场景
────────────────────────────────────────────────
常量折叠/传播 减少运行时计算 常量表达式
死代码消除 减少代码量 不可达代码
内联展开 消除函数调用开销 小函数、热点
循环优化 减少迭代开销 计算密集型
向量化 SIMD并行计算 数组处理
别名分析 解锁更多优化 指针代码
强度削减 用快操作替代慢操作 乘除法
核心原则:优化在保证程序语义不变的前提下,通过减少工作量、提高并行性、改善内存访问模式来提升性能。
GCC/Clang 优化等级详细对比
一、优化等级总览
text
-O0 ────────────────────────────────────────── 最慢,最易调试
-O1 ████
-O2 ████████████ ← 推荐生产环境
-O3 ████████████████████
-Os ██████████ (代码尺寸优先)
-Oz ████████ (代码尺寸最小, Clang独有)
-Og ████ (调试友好) ← 推荐开发环境
-Ofast ████████████████████████ ← 危险!破坏IEEE标准
性能提升 ──────────────────────────────►
二、逐级详细说明
-O0(无优化,默认)
C
// 源代码
int add(int a, int b) {
int result = a + b;
return result;
}
asm
; -O0 生成的汇编(x86-64)
add:
push rbp
mov rbp, rsp
mov DWORD PTR [rbp-20], edi ; 保存参数a到栈
mov DWORD PTR [rbp-24], esi ; 保存参数b到栈
mov edx, DWORD PTR [rbp-20] ; 读取a
mov eax, DWORD PTR [rbp-24] ; 读取b
add eax, edx ; a + b
mov DWORD PTR [rbp-4], eax ; 保存到result变量
mov eax, DWORD PTR [rbp-4] ; 读取result
pop rbp
ret
; ↑ 每个变量都老实存到栈上,完全对应源码
text
特点:
✓ 编译速度最快
✓ 调试信息完整,断点/单步完全准确
✓ 变量值可以直接在调试器中查看
✗ 运行速度最慢(多余的内存读写)
✗ 生成代码量最大
适用:日常开发、调试 Bug
-O1(基本优化)
在 -O0 基础上启用以下优化:
text
基本优化集合:
┌─────────────────────────────────────────────┐
│ ✓ 常量折叠 ✓ 死代码消除 │
│ ✓ 基本块合并 ✓ 局部公共子表达式消除 │
│ ✓ 简单内联 ✓ 尾调用优化(部分) │
│ ✓ 基本寄存器分配优化 │
└─────────────────────────────────────────────┘
C
// 源代码
int example() {
int x = 10;
int y = 20;
int z = x + y; // 常量表达式
if (0) {
printf("never"); // 死代码
}
return z;
}
asm
; -O1 生成的汇编
example:
mov eax, 30 ; 直接计算出结果
ret ; 死代码和中间变量全部消除
text
与 -O0 相比:
✓ 消除明显冗余
✓ 基本寄存器使用优化
✓ 编译时间略增加
~ 调试基本可用,但某些变量可能看不到
✗ 不做激进的循环优化
✗ 不做向量化
适用:对速度有轻微要求,但仍需调试
-O2(标准优化,推荐生产)
在 -O1 基础上增加大量优化:
text
-O2 新增的主要优化:
┌─────────────────────────────────────────────────────┐
│ 循环优化 │
│ ✓ 循环不变量外提 (LICM) │
│ ✓ 循环强度削减 │
│ │
│ 函数优化 │
│ ✓ 更激进的内联 │
│ ✓ 函数克隆 (针对常量参数) │
│ │
│ 分析优化 │
│ ✓ 别名分析 │
│ ✓ 全局公共子表达式消除 │
│ ✓ 部分冗余消除 (PRE) │
│ │
│ 代码生成 │
│ ✓ 指令调度 │
│ ✓ 分支预测优化 │
│ ✓ if-conversion (分支转无分支) │
└─────────────────────────────────────────────────────┘
C
// 循环不变量外提示例
// 源码
for (int i = 0; i < n; i++) {
arr[i] += sqrt(x) * factor; // sqrt(x)是循环不变量
}
// -O2 优化后等效代码
double tmp = sqrt(x) * factor; // 提到循环外
for (int i = 0; i < n; i++) {
arr[i] += tmp;
}
C
// 分支转无分支 (if-conversion)
// 源码
int abs_val(int x) {
if (x < 0) return -x;
return x;
}
// -O2 汇编(无分支跳转)
abs_val:
mov eax, edi
neg eax
cmovns eax, edi ; 条件移动指令,避免分支预测失败
ret
text
与 -O1 相比:
✓ 性能提升显著(通常 20-50%)
✓ 大多数重要优化都包含
✓ 不破坏 IEEE 浮点标准
✓ 不做可能增大代码体积很多的优化
~ 调试较困难(变量可能被优化掉)
✗ 默认不做向量化(GCC需要-ftree-vectorize)
适用:绝大多数生产环境 ← 首选
-O3(激进优化)
在 -O2 基础上增加:
text
-O3 新增的主要优化:
┌─────────────────────────────────────────────────────┐
│ ✓ 自动向量化 (Auto-vectorization) │
│ - 利用 SSE/AVX/NEON 等 SIMD 指令 │
│ │
│ ✓ 更激进的内联 (更大的函数也内联) │
│ │
│ ✓ 循环变换 │
│ - 循环展开 (Loop unrolling) │
│ - 循环向量化 │
│ - 循环分裂/融合 │
│ │
│ ✓ 推测执行优化 │
│ ✓ 更多函数克隆 │
└─────────────────────────────────────────────────────┘
C
// 向量化示例
// 源码
void add_arrays(float* a, float* b, float* c, int n) {
for (int i = 0; i < n; i++)
c[i] = a[i] + b[i];
}
asm
; -O3 自动向量化后的汇编(AVX2)
; 一次处理 8 个 float
vmovups ymm0, [rdi] ; 加载 8 个 float from a
vaddps ymm0, ymm0, [rsi] ; 8个float同时相加
vmovups [rdx], ymm0 ; 存储 8 个 float to c
; 循环步进 8,速度提升 ~8倍
text
-O3 的风险:
┌──────────────────────────────────────────────────┐
│ ⚠ 代码体积可能显著增大(内联、循环展开) │
│ ⚠ 编译时间明显增加 │
│ ⚠ 激进内联可能导致指令缓存压力增大 │
│ ⚠ 某些情况下比 -O2 更慢!(代码膨胀) │
│ ⚠ 向量化可能引入对齐要求 │
└──────────────────────────────────────────────────┘
适用:科学计算、图形处理、音视频编解码等计算密集型
-Os(优化代码大小)
text
策略:使用 -O2 的大多数优化,但禁用会增大代码体积的优化
启用:所有 -O2 优化中不增大体积的部分
禁用:
✗ 循环展开
✗ 函数内联(激进部分)
✗ 函数克隆
✗ 向量化(某些情况)
C
// 对比示例
void process(int* arr, int n) {
for (int i = 0; i < n; i++) {
arr[i] = arr[i] * 2 + 1;
}
}
asm
; -O3(展开后,代码更大但更快)
; 每次处理4个元素
add rdi, 0
...循环展开的代码...
; -Os(紧凑循环)
process:
test esi, esi
jle .done
.loop:
lea eax, [rdi*2+1] ; 紧凑的单次迭代
mov [rdi], eax
add rdi, 4
dec esi
jnz .loop
.done:
ret
text
适用场景:
✓ 嵌入式系统(Flash/ROM 受限)
✓ 共享库(减少加载时间)
✓ 移动端应用(安装包大小)
✓ 缓存受限的环境(小代码=更好的指令缓存命中)
-Oz(最小化代码,Clang 独有)
text
比 -Os 更激进地缩减代码体积
在 -Os 基础上:
✗ 禁用几乎所有内联
✗ 更激进的尺寸削减
✓ 可能使用更短的指令序列(即使更慢)
text
-O2 代码大小:100%
-Os 代码大小:~70-80%
-Oz 代码大小:~60-70%
适用:WebAssembly、固件、极度受限环境
-Og(调试优化,GCC 独有)
text
目标:在保持良好调试体验的前提下,提供一定优化
启用:不影响调试的安全优化
禁用:
✗ 变量消除(保持变量可观察)
✗ 激进内联(保持调用栈清晰)
✗ 代码重排(保持源码行对应)
text
调试质量对比:
变量可见性 调用栈清晰 断点准确 性能
-O0 ████████ ████████ ████████ ░░
-Og ███████ ███████ ███████ ██
-O1 █████ █████ █████ ████
-O2 ███ ███ ████ ████████
-O3 ██ ██ ███ ██████████
text
适用:日常开发(替代 -O0,更快的开发构建)
GCC 推荐:开发用 -Og,发布用 -O2
-Ofast(危险优化)
text
= -O3 + 违反标准的浮点优化 + 其他激进假设
额外启用:
✓ -ffast-math 浮点重排序(违反 IEEE 754)
✓ -fno-trapping-math 忽略浮点异常
✓ -ffinite-math-only 假设无 NaN/Inf
✓ -fassociative-math 假设浮点结合律(数学上对,计算机上错)
✓ -fno-signed-zeros 忽略 -0.0
C
// -Ofast 破坏浮点语义的例子
float sum = 0;
for (int i = 0; i < n; i++)
sum += arr[i];
// -Ofast 可能重排为:
// (arr[0]+arr[2]) + (arr[1]+arr[3]) + ...
// 浮点加法不满足结合律,结果不同!
// 具体例子
float a = 1e38, b = -1e38, c = 1.0;
float result1 = (a + b) + c; // = 0 + 1 = 1.0
float result2 = a + (b + c); // = a + (-1e38+1) ≈ 0(精度损失)
// -Ofast 可能把 result1 算成 result2 的结果
text
⚠ -Ofast 使用场景:
✓ 科学计算(可接受微小误差)
✓ 游戏(不需要精确浮点)
✗ 金融计算(绝对不能用)
✗ 需要精确 NaN/Inf 处理
✗ 任何对浮点精度有要求的场合
三、各等级优化 Pass 对比表
text
优化技术 O0 O1 O2 O3 Os Oz Og
──────────────────────────────────────────────────────
常量折叠 ✗ ✓ ✓ ✓ ✓ ✓ ✓
死代码消除 ✗ ✓ ✓ ✓ ✓ ✓ ✓
基本内联 ✗ ✓ ✓ ✓ ✓ ✗ ✓
寄存器分配优化 ✗ ✓ ✓ ✓ ✓ ✓ ✓
循环不变量外提 ✗ ✗ ✓ ✓ ✓ ✓ ✗
全局公共子表达式消除 ✗ ✗ ✓ ✓ ✓ ✓ ✗
指令调度 ✗ ✗ ✓ ✓ ✓ ✓ ✗
分支预测优化 ✗ ✗ ✓ ✓ ✓ ✓ ✗
if-conversion ✗ ✗ ✓ ✓ ✓ ✓ ✗
自动向量化 ✗ ✗ △ ✓ △ ✗ ✗
激进循环展开 ✗ ✗ ✗ ✓ ✗ ✗ ✗
函数克隆 ✗ ✗ △ ✓ ✗ ✗ ✗
激进内联 ✗ ✗ ✗ ✓ ✗ ✗ ✗
快速浮点 ✗ ✗ ✗ ✗ ✗ ✗ ✗
△ = 有限启用
四、性能实测对比
C
// 测试程序:矩阵乘法
void matmul(float* A, float* B, float* C, int N) {
for (int i = 0; i < N; i++)
for (int j = 0; j < N; j++)
for (int k = 0; k < N; k++)
C[i*N+j] += A[i*N+k] * B[k*N+j];
}
text
N=512 矩阵乘法,Intel i7,GCC 12:
等级 时间(ms) 相对 -O0 代码大小
──────────────────────────────────────
-O0 4820ms 1.0x 较大
-O1 890ms 5.4x 中等
-O2 340ms 14.2x 中等
-O3 95ms 50.7x 较大(向量化)
-Os 420ms 11.5x 最小
-Ofast 88ms 54.8x 较大
注:实际结果因代码和硬件而异
五、如何选择
text
你的场景 推荐等级
──────────────────────────────────────────────────
日常开发 / 调试 Bug -O0 或 -Og
追求调试质量 + 一定性能 -Og
生产环境(通用) -O2 ← 最常用
科学计算 / 音视频 / 游戏 -O3
嵌入式 / 固件 / 包大小敏感 -Os
WebAssembly / 极限压缩 -Oz(Clang)
允许浮点误差的极限性能 -Ofast(谨慎)
六、进阶:组合使用
Bash
# 生产环境推荐组合
gcc -O2 \
-march=native \ # 针对当前CPU优化
-flto \ # 链接时优化(跨文件内联)
-fprofile-use \ # 使用PGO数据(需先采集)
program.c
# 调试推荐
gcc -Og \
-g3 \ # 最完整调试信息
-fsanitize=address \ # 内存错误检测
program.c
# 嵌入式推荐
gcc -Os \
-ffunction-sections \ # 每个函数单独段(链接器可删除未用函数)
-fdata-sections \
-Wl,--gc-sections \ # 链接时删除未用段
program.c
Bash
# 查看具体启用了哪些优化Pass
gcc -O2 -Q --help=optimizers | grep enabled
# 单独开关某个优化
gcc -O2 -fno-inline program.c # O2 但禁用内联
gcc -O2 -ftree-vectorize program.c # 手动开启向量化
gcc -O3 -fno-unroll-loops program.c # O3 但禁止循环展开
核心建议:
- 开发期用 -Og(GCC)或 -O0 -g
- 发布版用 -O2,再根据 profiling 数据决定是否升级到 -O3
- 不要盲目用 -O3,先测量,再决定
免责声明
本文档所有内容仅供安全研究、学术交流与技术学习使用,严禁用于任何未经授权的逆向破解、网络攻击、隐私窃取、恶意软件开发及其他违反《中华人民共和国网络安全法》《数据安全法》等法律法规的行为,使用者应确保已获得目标软件权利人的合法授权并自行承担因使用本文档内容所产生的一切法律责任与后果,作者不对任何直接或间接损害承担任何责任,继续阅读即视为您已知悉并同意上述全部条款。
浙公网安备 33010602011771号