LLVM 开发指南 / 第 8 章:优化管线
第 8 章:优化管线
“编译器优化不是魔法,而是对程序变换的系统性应用。”
8.1 优化级别详解
8.1.1 优化级别对照表
| 级别 | Flag | 优化策略 | 编译速度 | 运行速度 | 代码大小 |
|---|
| O0 | -O0 | 无优化 | ⚡⚡⚡⚡ | 🐢 | 📦📦📦📦 |
| O1 | -O1 | 基础优化 | ⚡⚡⚡ | 🏃🏃 | 📦📦📦 |
| O2 | -O2 | 标准优化 | ⚡⚡ | 🚀🚀 | 📦📦 |
| O3 | -O3 | 激进优化 | ⚡ | 🚀🚀🚀 | 📦📦📦 |
| Os | -Os | 优化大小 | ⚡⚡ | 🏃🏃 | 📦 |
| Oz | -Oz | 最小大小 | ⚡⚡ | 🏃 | 📦 |
8.1.2 各级别启用的主要 Pass
# 查看各优化级别启用的 Pass
opt -O0 -print-pipeline-passes input.ll -o /dev/null 2>&1 | head -20
opt -O2 -print-pipeline-passes input.ll -o /dev/null 2>&1 | head -50
opt -O3 -print-pipeline-passes input.ll -o /dev/null 2>&1 | head -80
| 优化技术 | O1 | O2 | O3 | Os | Oz |
|---|
| SimplifyCFG | ✅ | ✅ | ✅ | ✅ | ✅ |
| InstCombine | ✅ | ✅ | ✅ | ✅ | ✅ |
| SROA | ✅ | ✅ | ✅ | ✅ | ✅ |
| Inliner | ✅ | ✅ | ✅激进 | ✅ | ✅ |
| GVN | ❌ | ✅ | ✅ | ✅ | ✅ |
| LoopVectorize | ❌ | ✅ | ✅ | ❌ | ❌ |
| SLPVectorize | ❌ | ✅ | ✅ | ✅ | ❌ |
| LoopUnroll | ❌ | ✅ | ✅激进 | ✅保守 | ❌ |
| LoopInterchange | ❌ | ❌ | ✅ | ❌ | ❌ |
| LoopDistribute | ❌ | ❌ | ✅ | ❌ | ❌ |
| CallSiteSplitting | ❌ | ✅ | ✅ | ✅ | ✅ |
8.2 函数内联 (Function Inlining)
函数内联是最重要的优化之一,它可以消除函数调用开销,并为后续优化创造机会。
8.2.1 内联的基本原理
内联前: 内联后:
┌──────────────┐ ┌──────────────────┐
│ main() │ │ main() │
│ ... │ │ ... │
│ foo(x) │ ──► │ { // foo 的代码 │
│ ... │ │ a = x * 2; │
└──────────────┘ │ b = a + 1; │
│ } │
┌──────────────┐ │ ... │
│ foo(int x) │ └──────────────────┘
│ a = x * 2; │
│ b = a + 1; │
│ return b; │
└──────────────┘
8.2.2 内联决策
// Clang 中控制内联的属性
__attribute__((noinline)) void func_no_inline(); // 禁止内联
__attribute__((always_inline)) void func_always(); // 强制内联
// C++17 属性
[[gnu::always_inline]] void func();
[[gnu::noinline]] void func();
# 控制内联的命令行选项
clang -mllvm -inline-threshold=200 file.c # 调整内联阈值
clang -fno-inline file.c # 禁止内联
clang -mllvm -inlinehint-threshold=500 file.c # hint 函数阈值
8.2.3 内联成本模型
LLVM 的内联器使用成本模型决定是否内联:
| 因素 | 影响 |
|---|
| 基本块数量 | 越多越贵 |
| 指令数量 | 越多越贵 |
| 调用深度 | 深度调用链降低内联概率 |
| 是否热路径 | Profile 信息影响 |
| 函数属性 | always_inline 强制内联 |
| 是否有循环 | 有循环的函数成本更高 |
8.2.4 内联的影响
// 内联前:优化器看不到跨函数信息
int square(int x) { return x * x; }
int compute() { return square(42); } // 内联后可以直接优化为 1764
// 内联后:
int compute() { return 42 * 42; } // 常量折叠
// 进一步优化为:
int compute() { return 1764; }
8.3 常量传播与折叠
8.3.1 稀疏条件常量传播 (SCCP)
; 优化前
define i32 @test() {
entry:
%x = add i32 1, 2 ; x = 3
%y = mul i32 %x, 4 ; y = 12
%z = icmp sgt i32 %y, 10 ; z = true
br i1 %z, label %if, label %else
if:
ret i32 %y ; 一定返回 12
else:
ret i32 0 ; 死代码,永远不会执行
}
; 优化后
define i32 @test() {
entry:
ret i32 12
}
8.3.2 全局值编号 (GVN)
; 优化前 — 冗余计算
define i32 @redundant(i32 %a, i32 %b) {
entry:
%x = add i32 %a, %b
%y = add i32 %a, %b ; 与 %x 相同!
%z = mul i32 %x, %y
ret i32 %z
}
; 优化后 — GVN 消除冗余
define i32 @redundant(i32 %a, i32 %b) {
entry:
%x = add i32 %a, %b
%z = mul i32 %x, %x
ret i32 %z
}
8.4 向量化优化
8.4.1 自动向量化概述
LLVM 有两种自动向量化:
| 类型 | 说明 | 适用场景 |
|---|
| Loop Vectorizer | 循环向量化 | 数组运算、循环 |
| SLP Vectorizer | 超字级并行 | 连续独立运算 |
8.4.2 循环向量化
// 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];
}
}
; 向量化前(标量循环)
loop:
%i = phi i64 [ 0, %entry ], [ %i.next, %loop ]
%a.ptr = getelementptr float, ptr %a, i64 %i
%b.ptr = getelementptr float, ptr %b, i64 %i
%c.ptr = getelementptr float, ptr %c, i64 %i
%a.val = load float, ptr %a.ptr
%b.val = load float, ptr %b.ptr
%sum = fadd float %a.val, %b.val
store float %sum, ptr %c.ptr
%i.next = add i64 %i, 1
%cmp = icmp slt i64 %i.next, %n
br i1 %cmp, label %loop, label %exit
; 向量化后(4 路 SIMD)
vector.body:
%index = phi i64 [ 0, %entry ], [ %index.next, %vector.body ]
%a.ptr = getelementptr float, ptr %a, i64 %index
%b.ptr = getelementptr float, ptr %b, i64 %index
%c.ptr = getelementptr float, ptr %c, i64 %index
%a.vec = load <4 x float>, ptr %a.ptr
%b.vec = load <4 x float>, ptr %b.ptr
%sum.vec = fadd <4 x float> %a.vec, %b.vec
store <4 x float> %sum.vec, ptr %c.ptr
%index.next = add i64 %index, 4
%cmp = icmp slt i64 %index.next, %n
br i1 %cmp, label %vector.body, label %exit
8.4.3 向量化控制
// Clang pragma 控制
#pragma clang loop vectorize(enable) // 启用向量化
#pragma clang loop vectorize_width(8) // 指定向量宽度
#pragma clang loop interleave(enable) // 启用交错
#pragma clang loop interleave_count(4) // 交错因子
// OpenMP SIMD
#pragma omp simd
for (int i = 0; i < n; i++) {
c[i] = a[i] + b[i];
}
// GCC/Clang 属性
__attribute__((vector_size(16))) typedef float v4f;
# 控制向量化
clang -O2 -fvectorize file.c # 启用向量化
clang -O2 -fno-vectorize file.c # 禁用向量化
clang -O2 -mllvm -force-vector-width=8 file.c # 强制向量宽度
clang -O2 -Rpass=loop-vectorize file.c # 报告向量化
clang -O2 -Rpass-missed=loop-vectorize file.c # 报告未向量化
clang -O2 -Rpass-analysis=loop-vectorize file.c # 详细分析
8.4.4 SLP 向量化
// SLP 向量化:将连续独立运算打包
void process(float *out, float a0, float a1, float b0, float b1) {
// 这些独立运算可以打包为 SIMD
out[0] = a0 + b0;
out[1] = a1 + b1;
out[2] = a0 * b0;
out[3] = a1 * b1;
}
8.5 循环优化
8.5.1 循环不变量外提 (LICM)
; 优化前 — 循环内有不变量计算
loop:
%i = phi i64 [ 0, %entry ], [ %i.next, %loop ]
%invariant = load i32, ptr @global ; 每次循环都加载!
%val = add i32 %i, %invariant
%i.next = add i64 %i, 1
%cmp = icmp slt i64 %i.next, 100
br i1 %cmp, label %loop, label %exit
; 优化后 — 不变量外提到循环外
preheader:
%invariant = load i32, ptr @global
br label %loop
loop:
%i = phi i64 [ 0, %preheader ], [ %i.next, %loop ]
%val = add i32 %i, %invariant
%i.next = add i64 %i, 1
%cmp = icmp slt i64 %i.next, 100
br i1 %cmp, label %loop, label %exit
8.5.2 循环展开 (Loop Unroll)
; 优化前
loop:
%i = phi i32 [ 0, %entry ], [ %i.next, %loop ]
%sum = phi i32 [ 0, %entry ], [ %sum.next, %loop ]
call void @process(i32 %i)
%sum.next = add i32 %sum, %i
%i.next = add i32 %i, 1
%cmp = icmp slt i32 %i.next, 100
br i1 %cmp, label %loop, label %exit
; 优化后(展开因子 4)
loop:
%i = phi i32 [ 0, %entry ], [ %i.next, %loop ]
%sum = phi i32 [ 0, %entry ], [ %sum.next, %loop ]
call void @process(i32 %i)
%sum.1 = add i32 %sum, %i
%i.1 = add i32 %i, 1
call void @process(i32 %i.1)
%sum.2 = add i32 %sum.1, %i.1
%i.2 = add i32 %i.1, 1
call void @process(i32 %i.2)
%sum.3 = add i32 %sum.2, %i.2
%i.3 = add i32 %i.2, 1
call void @process(i32 %i.3)
%sum.next = add i32 %sum.3, %i.3
%i.next = add i32 %i.3, 1
%cmp = icmp slt i32 %i.next, 100
br i1 %cmp, label %loop, label %exit
# 控制循环展开
clang -O2 -mllvm -unroll-count=4 file.c # 指定展开因子
clang -O2 -mllvm -unroll-threshold=200 file.c # 调整展开阈值
clang -O2 -fno-unroll-loops file.c # 禁用展开
8.5.3 循环交换 (Loop Interchange)
// 优化前 — cache 不友好
for (int j = 0; j < N; j++)
for (int i = 0; i < N; i++)
A[i][j] = B[i][j] + 1;
// 优化后 — 循环交换,cache 友好
for (int i = 0; i < N; i++)
for (int j = 0; j < N; j++)
A[i][j] = B[i][j] + 1;
# 启用循环交换(O3 默认)
clang -O3 -mllvm -enable-loopinterchange file.c
8.5.4 归纳变量简化 (IndVar Simplify)
; 优化前 — 复杂的归纳变量
loop:
%i = phi i64 [ 0, %entry ], [ %i.next, %loop ]
%j = mul i64 %i, 4 ; j = i * 4
%ptr = getelementptr i32, ptr %arr, i64 %j
store i32 0, ptr %ptr
%i.next = add i64 %i, 1
%cmp = icmp slt i64 %i.next, 100
br i1 %cmp, label %loop, label %exit
; 优化后 — 归纳变量简化
loop:
%ptr = phi ptr [ %arr, %entry ], [ %ptr.next, %loop ]
store i32 0, ptr %ptr
%ptr.next = getelementptr i32, ptr %ptr, i64 1
%cmp = icmp ne ptr %ptr.next, %end.ptr
br i1 %cmp, label %loop, label %exit
8.5.5 循环优化总结
| 优化 | Pass 名称 | 作用 |
|---|
| LICM | licm | 循环不变量外提 |
| 循环展开 | loop-unroll | 减少循环开销 |
| 循环交换 | loop-interchange | 改善缓存局部性 |
| 归纳变量简化 | indvars | 简化归纳变量 |
| 循环旋转 | loop-rotate | do-while 形式 |
| 循环删除 | loop-deletion | 消除空循环 |
| 循环分发 | loop-distribute | 拆分循环以向量化 |
| 循环融合 | loop-fuse | 合并循环 |
| 循环展平 | loop-flatten | 嵌套循环展平 |
| 循环强度削减 | loop-reduce | 降低循环内运算强度 |
8.6 死代码消除
8.6.1 多层次死代码消除
// 层次一:DCE — 简单死代码消除
// 消除无副作用的未使用指令
int x = compute(); // 如果 x 未使用,可以消除
// 层次二:ADCE — 激进死代码消除
// 沿着控制流追踪,消除不被使用的分支
int y = 42;
if (y > 0) { // 如果 y 是常量,整个 if 可以简化
...
}
// 层次三:GlobalDCE — 全局死代码消除
// 消除模块中未使用的全局变量和函数
static void unused_function() { ... } // 可以删除
// 层次四:DSE — 死存储消除
// 消除被覆盖的存储
*p = 1; // 这个存储是死的
*p = 2; // 只有这个有效
8.7 过程间优化与 LTO
8.7.1 Link-Time Optimization (LTO)
LTO 在链接时跨编译单元进行优化,可以实现跨文件内联:
# 传统 LTO(Full LTO)
clang -flto -O2 -c a.c -o a.o
clang -flto -O2 -c b.c -o b.o
clang -flto -O2 a.o b.o -o program
# Thin LTO(推荐,编译更快)
clang -flto=thin -O2 -c a.c -o a.o
clang -flto=thin -O2 -c b.c -o b.o
clang -flto=thin -O2 a.o b.o -o program
# 查看 LTO 优化
clang -flto=thin -O2 -Wl,--save-temps a.o b.o -o program
8.7.2 Full LTO vs Thin LTO
| 特性 | Full LTO | Thin LTO |
|---|
| 链接速度 | 🐢 慢 | ⚡ 快 |
| 内存使用 | 📦📦📦 高 | 📦 低 |
| 优化质量 | 🚀🚀 略好 | 🚀 好 |
| 增量编译 | ❌ 不支持 | ✅ 支持 |
| 并行优化 | ❌ 有限 | ✅ 充分并行 |
| 推荐场景 | 极致优化 | 一般场景 |
8.7.3 LTO 的优化能力
无 LTO:
a.c → a.o (独立优化)
b.c → b.o (独立优化)
a.o + b.o → program (仅链接)
有 LTO:
a.c → a.bc
b.c → b.bc
a.bc + b.bc → 合并 LLVM IR → 全局优化 → program
LTO 可以做到:
✅ 跨文件内联
✅ 跨文件常量传播
✅ 跨文件死代码消除
✅ 跨文件别名分析
✅ 跨文件虚函数去虚拟化
8.8 Profile-Guided Optimization (PGO)
8.8.1 PGO 流程
# 步骤 1: 插桩编译
clang -fprofile-generate -O2 -o program_instrumented program.c
# 步骤 2: 运行收集 profile
./program_instrumented < typical_input.txt
# 生成 default.profraw
# 步骤 3: 合并 profile
llvm-profdata merge -output=program.profdata default.profraw
# 步骤 4: 使用 profile 优化编译
clang -fprofile-use=program.profdata -O2 -o program_optimized program.c
# 验证
./program_optimized
8.8.2 PGO 的优化效果
| 优化 | 无 PGO | 有 PGO |
|---|
| 内联决策 | 基于启发式 | 基于实际调用频率 |
| 分支预测 | 无信息 | 知道哪个分支更可能 |
| 基本块布局 | 默认顺序 | 热路径在前 |
| 循环展开 | 默认因子 | 根据执行次数调整 |
| 函数排序 | 默认 | 热函数聚集 |
8.8.3 BOLT 二进制优化
# BOLT — 基于 profile 的二进制后链接优化
# 需要链接时使用 --emit-relocs
# 编译时启用 relocations
clang -O2 -Wl,--emit-relocs -o program program.c
# 收集 profile (使用 perf)
perf record -e cycles:u -j any,u -o perf.data -- ./program
perf2bolt -p perf.data -o perf.fdata ./program
# BOLT 优化
llvm-bolt ./program -o program.bolt -data=perf.fdata
8.9 优化调试
# 查看优化前后的 IR
opt -O2 -S input.ll -o output.ll
# 查看特定优化的效果
opt -passes='instcombine' -S input.ll -o output.ll
# 查看优化报告
opt -passes='loop-vectorize' -pass-remarks='loop-vectorize' input.ll -o /dev/null 2>&1
# Clang 优化报告
clang -O2 -Rpass='.*' file.c 2>&1 # 成功的优化
clang -O2 -Rpass-missed='.*' file.c 2>&1 # 未应用的优化
clang -O2 -Rpass-analysis='.*' file.c 2>&1 # 详细分析
# 查看优化统计
opt -O2 -stats input.ll -o /dev/null 2>&1 | sort -t= -k2 -rn | head -20
8.10 本章小结
| 优化类别 | 主要技术 | 效果 |
|---|
| 内联 | Inliner Pass | 消除调用开销,暴露跨函数优化 |
| 常量传播 | SCCP, ConstantPropagation | 编译期计算常量 |
| 值编号 | GVN | 消除冗余计算 |
| 向量化 | LoopVectorize, SLPVectorize | SIMD 并行 |
| 循环优化 | LICM, Unroll, Interchange | 循环效率提升 |
| 死代码消除 | DCE, ADCE, DSE, GlobalDCE | 减少无效代码 |
| LTO | Full LTO, Thin LTO | 跨文件优化 |
| PGO | Profile-Guided Optimization | 基于实际运行数据优化 |
扩展阅读
- LLVM Optimization Remarks — 优化报告
- Auto-Vectorization in LLVM — 向量化文档
- Link Time Optimization — LTO 文档
- Profile Guided Optimization — PGO 文档
- BOLT — 二进制优化器
下一章: 第 9 章:代码生成 — 学习 LLVM 如何将 IR 转换为目标机器码。