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 转换为目标机器码。