目标:理解 AST→IR 的翻译过程,掌握 LLVM 核心优化 Pass 的原理和用法,能自己写 Pass。今天最关键——Day2 学了"怎么分析",今天学"分析完了怎么改 IR"。
上午:理论(龙书第 6 章 + 第 9 章)
1. 中间代码生成(第 6 章)—— 从 AST 到三地址码
1.1 为什么不直接从 AST 生成机器码?
因为目标架构太多了——x86、ARM、RISC-V 各有不同的指令集、寄存器、寻址方式。如果 AST 直接面对几十种 CPU,每个组合都要写一套翻译逻辑,组合爆炸。
中间表示(IR)= 一种"共识语言":所有前端翻译为 IR,所有后端从 IR 翻译到目标机。代价是前端+后端要各写 N+M 套,不是 N×M 套。
1.2 三地址码(Three-Address Code, TAC)
龙书的标准三地址码:
x = y op z 二元运算: 目标 = 源1 op 源2
x = op y 一元运算
x = y 拷贝
goto L 无条件跳转
if x goto L 条件跳转 (x 为真则跳)
if x relop y goto L 关系跳转
x = &y 取地址
*x = y 间接写
x = *y 间接读
x = call p, n 调用 p,n 个参数,结果放 x
param x 传参
LLVM IR 就是三地址码:
龙书 TAC: t1 = a + b; t2 = t1 * c; a = t2
LLVM IR: %t1 = add i32 %a, %b
%t2 = mul i32 %t1, %c
store i32 %t2, ptr %a
每行恰有一个操作符,恰有两个源操作数(或一个,但绝不超过三个地址)。
1.3 表达式翻译:AST 后序遍历
生成三地址码的标准算法——AST 后序遍历:
def translate_expr(node):
"""对表达式 AST 节点生成三地址码,返回存放结果的临时变量名"""
if node.type == 'NUM':
return str(node.value) # 常量直接返回
elif node.type == 'ID':
return node.name # 变量名直接返回
elif node.type in ('+', '-', '*', '/'):
left = translate_expr(node.left) # 递归翻译左子树
right = translate_expr(node.right) # 递归翻译右子树
temp = new_temp() # 分配临时变量
emit(f'{temp} = {left} {node.type} {right}') # 生成一条 TAC
return temp
elif node.type == 'ASSIGN':
right = translate_expr(node.right)
# 左值是赋值的"目标地址",不需要翻译为值
emit(f'{node.left.name} = {right}')
return None # 赋值语句没有返回值
示例——表达式 (a + b) * (c - d) 的 AST:
*
/ \
+ -
/ \ / \
a b c d
后序遍历翻译过程:
访 a → "a"
访 b → "b"
出 + → emit "t1 = a + b" → 返回 "t1"
访 c → "c"
访 d → "d"
出 - → emit "t2 = c - d" → 返回 "t2"
出 * → emit "t3 = t1 * t2" → 返回 "t3"
生成的三地址码:
t1 = a + b
t2 = c - d
t3 = t1 * t2
1.4 控制流翻译:短路求值与回填
短路求值(Short-Circuit):if (a && b) 如果 a 为假,b 根本不求值。
翻译的思路——给每条布尔表达式配两个标签:
E.true = 表达式 E 为真时跳到哪里
E.false = 表达式 E 为假时跳到哪里
对于 E1 && E2:
翻译 E1:如果 E1 为真 → 继续求值 E2
如果 E1 为假 → 整个 && 为假,跳转到 E.false
然后 E2 的 true/false 直接就是整个表达式的 true/false
实例——if (a > 0 && b > 0) { body } else { elses }:
; 龙书风格的三地址码
if a <= 0 goto Lfalse ; a > 0 不成立 → 短路,跳假分支
if b <= 0 goto Lfalse ; b > 0 不成立 → 跳假分支
; body 代码...
goto Lend
Lfalse:
; else 代码...
Lend:
; LLVM IR 等价形式
entry:
%cmp1 = icmp sgt i32 %a, 0
br i1 %cmp1, label %check_b, label %if.else
check_b:
%cmp2 = icmp sgt i32 %b, 0
br i1 %cmp2, label %if.then, label %if.else
if.then:
; body...
if.else:
; else...
if.end:
; 继续...
回填(Backpatching):单趟编译时,目标标签可能还没出现。
# 回填的核心数据结构
pending_jumps = [] # 待回填的跳转指令列表
def emit_jump_to_future():
"""生成一条跳转,目标暂时未知,留个坑"""
instr = "goto ???" # 目标地址待定
idx = len(code)
code.append(instr)
pending_jumps.append(idx)
def backpatch_label(label_idx):
"""现在知道了目标位置,把所有待定的跳转填上"""
for jump_idx in pending_jumps:
code[jump_idx] = f"goto L{label_idx}"
Clang 的代码生成模块(clang/lib/CodeGen/CGStmt.cpp)在处理 if、while、switch 时大量使用回填——先留 BasicBlock* 空指针,等目标块创建后再接上。
1.5 类型检查与符号表
语义分析阶段的核心工作:
def check_binary_expr(op, left, right):
"""简单类型检查"""
lt = left.type
rt = right.type
if op in ('+', '-', '*', '/'):
if lt == 'int' and rt == 'int':
return 'int'
elif lt == 'float' and rt == 'float':
return 'float'
elif lt == 'int' and rt == 'float':
# 隐式类型转换: int → float
emit(f't = int_to_float({left.name})')
return 'float'
else:
raise TypeError(f"不支持 {op} 对 {lt} 和 {rt}")
在 Clang 中,这个逻辑分布在 Sema(Semantic Analysis)模块。SemaExpr.cpp 有超过 2 万行,处理 C++ 的所有隐式转换、重载决议、模板推导。
2. 机器无关优化(第 9 章)—— 数据流分析的应用
第 4 章讲了数据流分析的框架(in/out/gen/kill/∧),第 9 章是在这个框架上实现具体优化。
2.1 全局公共子表达式消除(GCSE)
问题:如果 a+b 在两个地方都计算了,且操作数没变,第二次可以复用第一次的结果。
依赖分析:可用表达式分析(前向 ∩)
// 优化前
x = a + b; // 定义 e1 = a+b
y = c + d;
z = a + b; // a+b 又一次出现!但 a 和 b 都没变 → 这是公共子表达式
// 优化后
x = a + b;
y = c + d;
z = x; // 直接复用 x
LLVM 实现:GVN Pass(Global Value Numbering)就是 GCSE 的超集——它不仅消除公共子表达式,还能做常量折叠、代数简化。
# 看 GVN 的效果
cat > test_gvn.c << 'EOF'
int foo(int a, int b, int c) {
int x = a + b;
int y = c * 2;
int z = a + b; // 公共子表达式
return x + z;
}
EOF
clang -S -emit-llvm -O0 test_gvn.c -o test_gvn.ll
opt -passes=gvn test_gvn.ll -S -o test_gvn_opt.ll
# 观察:z = a+b 会被替换为 z = x
2.2 部分冗余消除(PRE)—— 集大成者
GCSE 只能消除 所有路径上都重复 的计算。但有时一条路径重复,另一条不重复——这叫部分冗余。
// 部分冗余的例子
if (cond) {
x = a + b; // 路径 A:计算了 a+b
}
// ...
if (cond) {
y = a + b; // 路径 B:又算了 a+b。如果 cond 为真,这是第二次
}
PRE 的处理方式:
// PRE 优化后(代码提升)
if (cond) {
x = a + b; // 保留
}
// ...
t = a + b; // 提取到公共路径!
if (cond) {
y = t; // 直接用
} else {
// 如果 cond 为假,第一条路径没算 a+b,但 t 已算好,可以直接用
}
PRE 综合了:
- 公共子表达式消除(完全冗余 → 直接删重复)
- 循环不变量外提(循环内的冗余 → 提到 preheader)
- 代码下沉(只在某些路径需要 → 下沉到需要的路径)
关键理解:PRE 是数据流分析的"终极应用"——它需要同时跑可用表达式(前向∩)和 anticipatable 表达式(后向∩),然后做插入+删除。算法复杂度 O(N³) 但有近似 O(N²) 的实现。LLVM 的 GVN Pass 不直接做完整的 PRE,但结合 GVN + LICM + Sink 可以达到类似效果。
2.3 循环优化——几个必须掌握的变换
a) 循环不变量外提(LICM, Loop-Invariant Code Motion)
// 优化前
for (int i = 0; i < n; i++) {
x = a * b; // a 和 b 在循环内不变!
arr[i] = x + i;
}
// 优化后
x = a * b; // 提到循环外(放入 preheader)
for (int i = 0; i < n; i++) {
arr[i] = x + i;
}
LLVM 的实现逻辑:对每条指令,检查其操作数是否都来自循环外或循环不变量。如果是,就足够外提——但还要检查支配条件(外提后会不会在原本不执行的路径上被求值)和寄存器压力(外提太多会溢出)。
opt -passes=licm input.ll -S -o output.ll
b) 归纳变量消除(Induction Variable Elimination)
// 优化前:i 是循环变量,j = i * 4 是派生归纳变量
for (int i = 0; i < 100; i++) {
j = i * 4;
arr[j] = 0; // 等价于 arr[i * 4] = 0
}
// 优化后:用 arr_ptr 的步进代替 i*4
int *ptr = &arr[0];
for (int i = 0; i < 100; i++) {
*ptr = 0;
ptr++; // 用指针自增代替每次的乘法
}
强度削弱(Strength Reduction):把昂贵的操作(乘法)替换为便宜的(加法)。LLVM 的 indvars Pass 处理这一切。
c) 循环展开(Loop Unrolling)
// 展开前(每次迭代处理 1 个元素)
for (int i = 0; i < 100; i++) {
sum += arr[i];
}
// 展开后(每次迭代处理 4 个元素)
for (int i = 0; i < 100; i += 4) {
sum += arr[i];
sum += arr[i+1];
sum += arr[i+2];
sum += arr[i+3];
}
收益:减少分支次数(100 次 → 25 次),改善指令级并行度。代价:代码膨胀。LLVM 自动权衡。
opt -passes='loop-unroll' input.ll -S
d) 循环向量化(Loop Vectorization)
SIMD 指令一次处理 4 个(或 8 个、16 个)元素:
; 标量版本
for.body:
%v = load i32, ptr %ptr
%sum = add i32 %sum, %v
; 向量化版本(一次处理 4 个 i32)
vector.body:
%wide.load = load <4 x i32>, ptr %ptr ; 一次加载 4 个 int
%sum.vec = add <4 x i32> %sum.vec, %wide.load ; 一次加 4 对 int
clang -O2 -Rpass=loop-vectorize loop.c # 看 LLVM 报告哪些循环被向量化了
下午:实战(LLVM 书第 6 章 + 第 9 章)
3. TableGen(第 6 章)—— 不是"配置文件",是 DSL
3.1 为什么需要 TableGen
LLVM 后端需要描述的信息量巨大。以 x86 为例——上千条指令,每条有不同的编码、操作数、延迟、流水线行为。手写 C++ 不仅冗长,还容易出错。
TableGen 是一种领域特定语言(DSL),专门用来描述目标架构信息:
.td 文件(声明式描述) → llvm-tblgen 工具 → C++ 头文件/源文件(自动生成)
3.2 TableGen 语法速览
// ---- 基本类型 ----
// bit: 0 或 1
// int: 整数
// string: 字符串
// list<T>: 列表
// dag: 有向无环图(用于模式匹配)
// ---- 类定义(class)----
// 类似 C++ 的类,可以有模板参数
class Instruction<string mnemonic, bits<8> opcode> {
string Mnemonic = mnemonic; // 助记符
bits<8> Opcode = opcode; // 操作码
bit isTerminator = 0; // 默认不是基本块结束指令
bit isBranch = 0;
bit hasSideEffects = 0;
}
// ---- 定义(def)----
// 类的实例
def ADD : Instruction<"add", 0x01> {
let isTerminator = 0;
}
// ---- 多类(multiclass)----
// 一次生成多个 def,用模板参数
multiclass ArithI<string mnemonic, bits<8> opcode> {
def rr : Instruction<mnemonic, opcode>; // 寄存器-寄存器版本
def ri : Instruction<mnemonic, opcode>; // 寄存器-立即数版本
}
defm ADD : ArithI<"add", 0x01>; // 生成 ADD_rr 和 ADD_ri
// ---- DAG 模式(指令选择的核心)----
// [(set 目标, (IR操作 源操作数))] 描述这条指令匹配什么样的 IR
def ADDri : Inst<(outs GR32:$rd), (ins GR32:$rs, i32imm:$imm),
"add $rd, $rs, $imm",
[(set i32:$rd, (add i32:$rs, imm:$imm))]>;
// ↑ 意思是:当 IR 中有 (add i32:rs, 常量:imm) 且结果赋给 i32:rd →
// ↓ 用这条指令编码,输出 "add rd, rs, imm"
3.3 TableGen 生成的 C++ 代码
运行 llvm-tblgen 可以看到生成的代码:
# 以 RISC-V 后端为例
cd llvm/lib/Target/RISCV/
llvm-tblgen RISCV.td -I ../../../include -gen-instr-info
# 输出类似(精简):
# const MCInstrDesc RISCVInsts[] = {
# { RISCV::ADD, "add", ... },
# { RISCV::SUB, "sub", ... },
# ...
# };
生成的代码包含:
- 指令描述表(
MCInstrDesc) - 寄存器信息(
MCRegisterInfo) - 调用约定信息(
CallingConv) - 指令选择匹配表(
MatcherTable—— 自动机驱动的模式匹配)
关键认知:后端的指令选择器(SelectionDAG ISel)不直接读 .td 文件——它在运行时遍历一个自动机形式的匹配表,这个表是 llvm-tblgen 从 DAG 模式编译生成的。
4. SSA 优化 Pass 体系(第 9 章)—— 核心 Pass 逐个剖析
这一节是 Day3 最重要的实战内容。我们对照 .ll 文件,逐个跑 LLVM 标准优化 Pass,理解每一步做了什么、怎么做的。
4.1 mem2reg —— 构建 SSA 的入口 Pass
这是所有优化 Pass 的前置条件。它把 alloca/load/store 模式提升为 SSA 寄存器。
# 生成未优化的 IR
clang -S -emit-llvm -O0 -Xclang -disable-O0-optnone example.c -o example.ll
# 跑 mem2reg
opt -passes=mem2reg example.ll -S -o example_ssa.ll
算法回顾(从 Day2 的知识出发):
- 对每个
alloca变量,找所有store的块 - 在
DF[store块]处插入 φ 节点(迭代直到不动点) - 用 φ 参数和 store 值重命名所有使用(改名遍)
- 删除所有
alloca、不会到达任何load的store、被 φ 替代的load
# 看 mem2reg 干了什么(需要 debug 版 LLVM)
opt -passes='print<domtree>,mem2reg,print<domtree>' example.ll -S
4.2 GVN(Global Value Numbering)—— 消除冗余
GVN 同时做全局公共子表达式消除 + 常量折叠 + 代数简化。
核心思想——值编号(Value Numbering):给每个"值"分配一个编号。如果 a+b 和 c+d 有相同的值编号(操作数同号,操作符相同),则它们等价。
// 输入
int foo(int a, int b) {
int x = a + b;
int y = a; // y 和 a 同值
int z = y + b; // z = a + b = x
return x + z; // = x + x = 2*x
}
// GVN 输出(等价于)
int foo(int a, int b) {
int x = a + b;
return x + x;
}
opt -passes=gvn example.ll -S
4.3 LICM —— 循环不变量外提
算法:
- 找出所有循环不变指令:操作数来自循环外或循环不变指令
- 对每条不变指令,检查能否移到 preheader(需要支配所有使用点)
- 如果能,移到 preheader
// 输入
for (int i = 0; i < n; i++) {
x = a * b; // a 和 b 在循环中不变 → 循环不变
arr[i] = x + i;
}
// LICM 后
x = a * b;
for (int i = 0; i < n; i++) {
arr[i] = x + i;
}
opt -passes=licm example.ll -S
4.4 DCE(Dead Code Elimination)—— 死代码删除
基于活跃变量分析的结果:如果一条指令定义了某个值,而这个值从不在任何"有用"的地方被引用,这条指令就是死代码。
; 输入
%x = mul i32 %a, %b ; %x 后面从未使用
%y = add i32 %c, %d ; %y 在 ret 中用到 → 不是死代码
ret i32 %y
; DCE 后
%y = add i32 %c, %d
ret i32 %y ; %x 的指令被删了
DCE 的 “涟漪效应”:删除一条指令后,它的操作数可能也变成死代码,需要迭代删除。LLVM 用 worklist 算法实现。
opt -passes=dce example.ll -S
4.5 simplifycfg —— CFG 简化
这个 Pass 做多种 CFG 级别的简化:
a) 跳转到跳转 → 直接跳转
; 简化前
br label %A
A:
br label %B
; 简化后
br label %B
b) 只有一个前驱的基本块 → 合并
; 简化前
br label %A
A:
%x = add i32 1, 2
ret i32 %x
; 简化后
%x = add i32 1, 2
ret i32 %x
c) 条件分支的恒等式消除(If-Conversion)
; 简化前
br i1 true, label %A, label %B
; 简化后
br label %A ; 不可达的分支被删
d) switch → 跳转表 或 条件分支树,根据 case 的数量和密度自动选择。
opt -passes=simplifycfg example.ll -S
4.6 SROA(Scalar Replacement of Aggregates)
把小的结构体拆成标量成员:
// 输入
struct Point { int x; int y; };
struct Point p = {1, 2};
return p.x + p.y;
// SROA 后
int p_x = 1;
int p_y = 2;
return p_x + p_y; // 结构体完全消失
opt -passes=sroa example.ll -S
4.7 内联(Inliner)
把被调用函数体直接嵌入调用点:
// 内联前
static int square(int x) { return x * x; }
int foo(int a) { return square(a) + square(a+1); }
// 内联后
int foo(int a) {
int t1 = a * a; // square(a) 的内联体
int t2 = (a+1) * (a+1); // square(a+1) 的内联体
return t1 + t2;
}
内联的代价模型:不是越大越好——内联太多→代码膨胀→指令缓存miss增加。LLVM 根据函数体大小、调用次数、调用点热度综合决定。
opt -passes=inline example.ll -S
4.8 O2 管道——这些 Pass 如何组合
# 查看 O2 的 Pass 序列
opt -passes='default<O2>' example.ll -S --debug-pass=Structure 2>&1 | head
简化版的 O2 Pass 管道(实际更复杂,此处为逻辑顺序):
Inliner(内联热点函数)
→ SROA(拆分聚合类型)
→ EarlyCSE(公共子表达式消除:简单快速版)
→ simplifycfg(简化控制流)
→ mem2reg(SSA 构造:最关键的一步)
→ GVN(全局值编号:消除冗余计算)
→ LICM(循环不变量外提)
→ indvars(归纳变量规范化 + 消除)
→ loop-unroll(选择性循环展开)
→ loop-vectorize(循环向量化:SIMD)
→ instcombine(指令合并:局部代数简化)
→ DCE(死代码消除:清理)
→ simplifycfg(再次简化控制流)
每个 Pass 只做一件事,但叠加效果惊人——这就是 Pass Pipeline 的设计哲学。
5. 写一个 LLVM Pass(完整实战)
这是今天最重要的一节。我们写两个 Pass:一个只读分析(统计指令),一个变换 IR(替换加法为减法)。
5.1 环境准备
# 推荐用 LLVM 源码构建的开发环境
git clone https://github.com/llvm/llvm-project.git
cd llvm-project
git checkout llvmorg-18.1.0
mkdir build && cd build
# CMake 配置(Debug + 必要的组件)
cmake -G Ninja ../llvm \
-DCMAKE_BUILD_TYPE=Debug \
-DLLVM_ENABLE_PROJECTS="clang" \
-DLLVM_TARGETS_TO_BUILD="X86" \
-DCMAKE_INSTALL_PREFIX=./install
ninja opt clang # 只需编译 opt 和 clang
如果不编译源码,也可以用系统安装的 LLVM 加载插件:
# 确认 llvm-config 可用
llvm-config --cxxflags --ldflags --libs core irreader passes support
5.2 Pass 一:指令统计(只读分析 Pass)
// InstCount.cpp —— 统计每个函数的指令数(New PassManager 写法)
#include "llvm/IR/Function.h"
#include "llvm/IR/InstIterator.h" // inst_iterator: 遍历所有指令
#include "llvm/Passes/PassBuilder.h"
#include "llvm/Passes/PassPlugin.h"
#include "llvm/Support/raw_ostream.h"
using namespace llvm;
// ── Pass 本体 ──────────────────────────────────────
namespace {
struct InstCountPass : public PassInfoMixin<InstCountPass> {
// 这是入口:对每个 Function 调用一次
PreservedAnalyses run(Function &F, FunctionAnalysisManager &) {
int total = 0;
int alloca_count = 0, load_count = 0, store_count = 0;
int branch_count = 0, phi_count = 0, call_count = 0;
for (auto &BB : F) { // 遍历每个基本块
for (auto &I : BB) { // 遍历块内每条指令
total++;
// 用 isa<> / dyn_cast<> 做类型判断
if (isa<AllocaInst>(&I)) alloca_count++;
if (isa<LoadInst>(&I)) load_count++;
if (isa<StoreInst>(&I)) store_count++;
if (isa<BranchInst>(&I)) branch_count++;
if (isa<PHINode>(&I)) phi_count++;
if (isa<CallInst>(&I)) call_count++;
}
}
errs() << "=== " << F.getName() << " ===\n"
<< " 总计: " << total << " 条指令\n"
<< " alloca: " << alloca_count
<< " load: " << load_count
<< " store: " << store_count
<< " branch: " << branch_count
<< " phi: " << phi_count
<< " call: " << call_count << "\n\n";
// 我们没修改 IR,返回 all() 表示所有分析结果仍然有效
return PreservedAnalyses::all();
}
};
}
// ── 注册 Pass 到 PassBuilder ───────────────────────
// 注册后可以用: opt -passes="instcount" hello.ll
extern "C" LLVM_ATTRIBUTE_WEAK ::llvm::PassPluginLibraryInfo
llvmGetPassPluginInfo() {
return {
LLVM_PLUGIN_API_VERSION, // API 版本
"InstCount", // 插件名
LLVM_VERSION_STRING, // LLVM 版本
[](PassBuilder &PB) { // 注册回调
PB.registerPipelineParsingCallback(
[](StringRef Name, FunctionPassManager &FPM,
ArrayRef<PassBuilder::PipelineElement>) {
if (Name == "instcount") {
FPM.addPass(InstCountPass());
return true;
}
return false;
});
}
};
}
编译:
# 编译为共享库
clang++ -fPIC -shared InstCount.cpp -o InstCount.so \
`llvm-config --cxxflags --ldflags --libs core irreader passes support`
# 运行
opt -load-pass-plugin=./InstCount.so \
-passes="instcount" hello.ll -disable-output
输出示例:
=== add ===
总计: 1 条指令
alloca: 0 load: 0 store: 0 branch: 0 phi: 0 call: 0
=== main ===
总计: 12 条指令
alloca: 2 load: 3 store: 2 branch: 2 phi: 0 call: 1
5.3 Pass 二:变换 IR——加法变减法
一个真正修改 IR 的 Pass。把所有 add 指令替换为等价的减法(仅供教学,无实际意义):
// AddToSub.cpp —— 把所有整数 add 替换为 sub
#include "llvm/IR/Function.h"
#include "llvm/IR/IRBuilder.h" // IRBuilder: 方便的指令构造器
#include "llvm/IR/InstrTypes.h"
#include "llvm/IR/Instructions.h"
#include "llvm/Passes/PassBuilder.h"
#include "llvm/Passes/PassPlugin.h"
#include "llvm/Transforms/Utils/BasicBlockUtils.h"
using namespace llvm;
namespace {
struct AddToSubPass : public PassInfoMixin<AddToSubPass> {
PreservedAnalyses run(Function &F, FunctionAnalysisManager &) {
bool changed = false;
for (auto &BB : F) {
for (auto &I : BB) {
// 找到整数 add 指令(不包括浮点 fadd)
if (auto *addInst = dyn_cast<BinaryOperator>(&I)) {
if (addInst->getOpcode() == Instruction::Add) {
// 创建减法指令: x + y → x - (-y)
IRBuilder<> builder(addInst);
Value *neg = builder.CreateNeg(addInst->getOperand(1),
"neg"); // -y
Value *sub = builder.CreateSub(addInst->getOperand(0),
neg, "sub"); // x - (-y)
// 替换所有使用
addInst->replaceAllUsesWith(sub);
changed = true;
}
}
}
}
// 返回被 Preservation 的分析
// 因为我们修改了 IR,支配树和循环信息可能失效
PreservedAnalyses PA;
if (!changed) {
PA.preserveSet<CFGAnalyses>(); // 没修改,保留所有
}
return PA;
}
};
}
extern "C" LLVM_ATTRIBUTE_WEAK ::llvm::PassPluginLibraryInfo
llvmGetPassPluginInfo() {
return {
LLVM_PLUGIN_API_VERSION, "AddToSub", LLVM_VERSION_STRING,
[](PassBuilder &PB) {
PB.registerPipelineParsingCallback(
[](StringRef Name, FunctionPassManager &FPM,
ArrayRef<PassBuilder::PipelineElement>) {
if (Name == "add-to-sub") {
FPM.addPass(AddToSubPass());
return true;
}
return false;
});
}
};
}
运行验证:
cat > test.c << 'EOF'
int foo(int a, int b) { return a + b; }
EOF
clang -S -emit-llvm -O2 -Xclang -disable-O0-optnone test.c -o test.ll
opt -load-pass-plugin=./AddToSub.so -passes="add-to-sub" test.ll -S -o out.ll
# 验证: a + b 变成了 a - (-b)
cat out.ll
5.4 Pass 开发核心知识清单
| 操作 | 代码 |
|---|---|
| 遍历所有函数 | for (auto &F : M) (Module 级 Pass 时) |
| 遍历基本块 | for (auto &BB : F) |
| 遍历指令 | for (auto &I : BB) |
| 类型判断 | isa<AllocaInst>(&I), isa<PHINode>(&I) |
| 类型转换 | auto *AI = dyn_cast<AllocaInst>(&I) |
| 创建指令 | IRBuilder<> builder(BB.getFirstNonPHI()); Value *v = builder.CreateAdd(a,b); |
| 替换使用 | old->replaceAllUsesWith(new); |
| 删除指令 | I.eraseFromParent(); |
| 创建基本块 | BasicBlock::Create(Context, "name", &F); |
| 获取上下文 | LLVMContext &ctx = F.getContext(); |
| 分配临时变量 | 不需要!SSA 自动管理 |
分析结果保存声明:
// 如果 Pass 没修改 IR(只读分析)
return PreservedAnalyses::all();
// 如果 Pass 修改了 IR 但没改 CFG
PreservedAnalyses PA;
PA.preserveSet<CFGAnalyses>(); // 支配树 & 循环信息仍然有效
return PA;
// 如果 Pass 大改了 IR
return PreservedAnalyses::none(); // 一切分析都重新计算
6. 练习
练习 1:观察 O2 管道效果(20 分钟)
# 写一个含 if/else + 循环 + 冗余计算的 C 函数
cat > pipeline_test.c << 'EOF'
int test(int a, int b, int n) {
int sum = 0;
int x = a * b; // 循环不变!
for (int i = 0; i < n; i++) {
int y = a * b; // x 的冗余!
sum = sum + i + y;
x = a * b; // x 的又一次冗余!
}
return sum;
}
EOF
# 对比 -O0 和 -O2 的 IR
clang -S -emit-llvm -O0 pipeline_test.c -o pipeline_O0.ll
clang -S -emit-llvm -O2 pipeline_test.c -o pipeline_O2.ll
# 逐步跑优化
opt -passes=mem2reg pipeline_O0.ll -S -o step1.ll # SSA 构造
opt -passes=mem2reg,gvn pipeline_O0.ll -S -o step2.ll # +冗余消除
opt -passes=mem2reg,gvn,licm pipeline_O0.ll -S -o step3.ll # +循环不变量外提
diff step1.ll step2.ll
diff step2.ll step3.ll
观察:
- step1:alloca 消失,出现 phi 节点(循环中的 i 和 sum)
- step2:
a*b的第二次出现被替换 - step3:
a*b被移到循环外
原始 C 代码分析:
int test(int a, int b, int n) {
int sum = 0;
int x = a * b; // x 在循环外定义(循环不变量!)
for (int i = 0; i < n; i++) {
int y = a * b; // y = a*b —— 和 x 的值完全一样(公共子表达式)
sum = sum + i + y; // sum 累加
x = a * b; // 又一次 a*b!和 x 和 y 都一样
}
return sum;
}
代码中有 3 处 a * b:循环外 x、循环内 y、循环内重赋值 x。但 a 和 b 在函数内从不被修改 → a*b 是循环不变量且是全局公共子表达式。
step0(-O0 原始 IR):
define i32 @test(i32 %a, i32 %b, i32 %n) {
entry:
; ... alloca: sum, x, i, y ...
store i32 0, ptr %sum
%mul = mul nsw i32 %a, %b ; 第1次: x = a * b
store i32 %mul, ptr %x
store i32 0, ptr %i
br label %for.cond
for.cond:
%i.val = load i32, ptr %i
%cmp = icmp slt i32 %i.val, %n
br i1 %cmp, label %for.body, label %for.end
for.body:
%mul2 = mul nsw i32 %a, %b ; 第2次: y = a * b ← 冗余!
store i32 %mul2, ptr %y
%sum.val = load i32, ptr %sum
%i.val2 = load i32, ptr %i
%y.val = load i32, ptr %y
%add1 = add nsw i32 %sum.val, %i.val2
%add2 = add nsw i32 %add1, %y.val
store i32 %add2, ptr %sum
%mul3 = mul nsw i32 %a, %b ; 第3次: x = a * b ← 冗余!
store i32 %mul3, ptr %x
br label %for.inc
for.inc:
%i.val3 = load i32, ptr %i
%inc = add nsw i32 %i.val3, 1
store i32 %inc, ptr %i
br label %for.cond
for.end:
%sum.final = load i32, ptr %sum
ret i32 %sum.final
}
3 个 mul nsw i32 %a, %b —— 但 O0 每条都是独立计算,不共享。
step1(mem2reg 后):
define i32 @test(i32 %a, i32 %b, i32 %n) {
entry:
%mul = mul nsw i32 %a, %b ; 循环外的 a*b
br label %for.cond
for.cond:
%sum.0 = phi i32 [ 0, %entry ], [ %add2, %for.inc ]
%i.0 = phi i32 [ 0, %entry ], [ %inc, %for.inc ]
%x.0 = phi i32 [ %mul, %entry ], [ %mul3, %for.inc ]
%cmp = icmp slt i32 %i.0, %n
br i1 %cmp, label %for.body, label %for.end
for.body:
%mul2 = mul nsw i32 %a, %b ; ← 还在循环内!
%add1 = add nsw i32 %sum.0, %i.0
%add2 = add nsw i32 %add1, %mul2
%mul3 = mul nsw i32 %a, %b ; ← 还在循环内!
br label %for.inc
for.inc:
%inc = add nsw i32 %i.0, 1
br label %for.cond
for.end:
ret i32 %sum.0
}
变化:alloca/load/store 全部消失,出现了 3 个 φ 节点(sum, i, x)。但 %mul2 和 %mul3 仍在循环内——mem2reg 不做优化,只做 SSA 构造。
step2(mem2reg + GVN 后):
define i32 @test(i32 %a, i32 %b, i32 %n) {
entry:
%mul = mul nsw i32 %a, %b ; 唯一的 a*b 计算
br label %for.cond
for.cond:
%sum.0 = phi i32 [ 0, %entry ], [ %add2, %for.inc ]
%i.0 = phi i32 [ 0, %entry ], [ %inc, %for.inc ]
%cmp = icmp slt i32 %i.0, %n
br i1 %cmp, label %for.body, label %for.end
for.body:
%add1 = add nsw i32 %sum.0, %i.0
%add2 = add nsw i32 %add1, %mul ; ← 直接用了 entry 的 %mul!
br label %for.inc
for.inc:
%inc = add nsw i32 %i.0, 1
br label %for.cond
for.end:
ret i32 %sum.0
}
变化:
%mul2和%mul3消失了——GVN 发现它们和 entry 中的%mul值编号相同x的 φ 节点也消失了(x 被 GVN 发现没用——只有赋值没有使用 → 后续 DCE 会删)- 函数从 5 个基本块减回 4 个
但是 %mul 虽然只在 entry 中计算了一次,IR 上还在循环外,不算"外提"问题——因为它本来就不在循环内。不过 step3 会用 LICM 确保类似的情况也能处理。
GVN 怎么做到的? a*b 的值编号是 VN(mul, VN(a), VN(b))。三处 a*b 的 a 和 b 都来自函数参数(从未被修改),值编号完全相同 → GVN 直接替换为第一个。
step3(mem2reg + GVN + LICM 后):
在这个例子中,step3 和 step2 的输出几乎一样——因为 %mul 本来就不在循环内。LICM 没有额外的工作可做。
但如果把原始代码改成 a*b 只在循环内出现:
// licm_only_test.c —— 循环不变但没有在循环外提前算
int test2(int a, int b, int n) {
int sum = 0;
for (int i = 0; i < n; i++) {
sum += a * b; // a*b 是循环不变但只在循环内第一次出现
}
return sum;
}
clang -S -emit-llvm -O0 licm_only_test.c -o licm_test.ll
opt -passes=mem2reg licm_test.ll -S -o licm_step1.ll
opt -passes=mem2reg,licm licm_test.ll -S -o licm_step2.ll
diff licm_step1.ll licm_step2.ll
LICM 前:%mul = mul i32 %a, %b 在 for.body 中(循环内)
LICM 后:%mul 被移到了 for.body.preheader(循环外的专用块),循环内直接用 %mul
最终 O2 IR 对比:
clang -S -emit-llvm -O2 pipeline_test.c -o pipeline_O2.ll
define i32 @test(i32 %a, i32 %b, i32 %n) {
entry:
%cmp4 = icmp sgt i32 %n, 0 ; n > 0?(小于等于则跳过循环)
br i1 %cmp4, label %for.body.preheader, label %for.end
for.body.preheader:
%mul = mul nsw i32 %a, %b ; ← a*b 只在 preheader 中算一次!
br label %for.body
for.body:
%i.06 = phi i32 [ %inc, %for.body ], [ 0, %for.body.preheader ]
%sum.05 = phi i32 [ %add, %for.body ], [ 0, %for.body.preheader ]
%add1 = add nsw i32 %sum.05, %i.06
%add = add nsw i32 %add1, %mul ; ← 直接用 preheader 的结果
%inc = add nuw nsw i32 %i.06, 1
%cmp = icmp eq i32 %inc, %n
br i1 %cmp, label %for.end.loopexit, label %for.body
for.end.loopexit:
%add.lcssa = phi i32 [ %add, %for.body ]
br label %for.end
for.end:
%sum.0.lcssa = phi i32 [ 0, %entry ], [ %add.lcssa, %for.end.loopexit ]
ret i32 %sum.0.lcssa
}
四个关键优化效果:
| 优化 | 体现 |
|---|---|
| 循环不变量外提 | a*b 从 3 处 → 1 处,且在 preheader(循环外) |
| 公共子表达式消除 | 循环内的两次 a*b 全部被消除 |
| 归纳变量优化 | i++ + i < n 变成了 %inc == %n(用 %inc 做循环出口条件,省了比较和归纳变量的维护) |
| LCSSA | %add.lcssa 是循环闭包 SSA 形式——确保循环内定义的值在循环外通过专用 φ 引用 |
指令数对比:O0 约 25 条 → O2 约 12 条,省了一半多。
练习 2:写一个分析 Pass(30 分钟)
实现一个 Pass,统计每个函数中"最深的循环嵌套层数":
提示:
- 用 LoopInfo 分析: auto &LI = FAM.getResult<LoopAnalysis>(F);
- LI.getLoopsInPreorder() 获取所有循环
- Loop::getLoopDepth() 获取嵌套深度
- 参考: llvm/include/llvm/Analysis/LoopInfo.h
▼ 练习 2 参考答案——完整 Pass 代码
// MaxLoopDepth.cpp —— 统计函数中最深的循环嵌套层数
#include "llvm/Analysis/LoopInfo.h"
#include "llvm/IR/Function.h"
#include "llvm/Passes/PassBuilder.h"
#include "llvm/Passes/PassPlugin.h"
#include "llvm/Support/raw_ostream.h"
using namespace llvm;
namespace {
struct MaxLoopDepthPass : public PassInfoMixin<MaxLoopDepthPass> {
// 需要 LoopInfo 分析结果 → 声明依赖
PreservedAnalyses run(Function &F, FunctionAnalysisManager &FAM) {
// 获取 LoopInfo 分析结果(FAM 自动管理缓存)
auto &LI = FAM.getResult<LoopAnalysis>(F);
int max_depth = 0;
int loop_count = 0;
// 遍历所有循环(前序遍历:外层先于内层)
for (auto *L : LI.getLoopsInPreorder()) {
loop_count++;
int depth = L->getLoopDepth();
if (depth > max_depth)
max_depth = depth;
// 额外信息:循环头基本块名、是否有子循环
errs() << " 循环 at "
<< L->getHeader()->getName()
<< " (深度 " << depth << ")";
if (!L->getSubLoops().empty())
errs() << " [含 " << L->getSubLoops().size() << " 个子循环]";
errs() << "\n";
}
errs() << "=== " << F.getName() << " ===\n"
<< " 循环总数: " << loop_count << "\n"
<< " 最大嵌套深度: " << max_depth << "\n\n";
// 只读 Pass,不修改 IR
return PreservedAnalyses::all();
}
// 声明:这个 Pass 需要 LoopAnalysis 的结果
// 这样 PassManager 会在运行此 Pass 前确保 LoopInfo 已计算
static void registerCallbacks() {} // 在注册时用下面的方法
};
}
// ── 注册 ──────────────────────────────────────
extern "C" LLVM_ATTRIBUTE_WEAK ::llvm::PassPluginLibraryInfo
llvmGetPassPluginInfo() {
return {
LLVM_PLUGIN_API_VERSION, "MaxLoopDepth", LLVM_VERSION_STRING,
[](PassBuilder &PB) {
// 注册为 Function Pass(Pipeline 元素)
PB.registerPipelineParsingCallback(
[](StringRef Name, FunctionPassManager &FPM,
ArrayRef<PassBuilder::PipelineElement>) {
if (Name == "max-loop-depth") {
FPM.addPass(MaxLoopDepthPass());
return true;
}
return false;
});
// 注册分析依赖:告诉 PassBuilder 我们需要 LoopAnalysis
PB.registerAnalysisRegistrationCallback(
[](FunctionAnalysisManager &FAM) {
FAM.registerPass([&] { return LoopAnalysis(); });
});
}
};
}
编译和使用:
# 编译
clang++ -fPIC -shared MaxLoopDepth.cpp -o MaxLoopDepth.so \
`llvm-config --cxxflags --ldflags --libs core analysis passes support`
# 准备测试用例
cat > nested_loops.c << 'EOF'
void matmul(int n, int m, int p, float A[n][m], float B[m][p], float C[n][p]) {
for (int i = 0; i < n; i++) // 深度 1
for (int j = 0; j < p; j++) // 深度 2
for (int k = 0; k < m; k++) // 深度 3
C[i][j] += A[i][k] * B[k][j];
}
void simple(int n) {
for (int i = 0; i < n; i++) // 深度 1
n--;
}
EOF
clang -S -emit-llvm -O0 nested_loops.c -o nested_loops.ll
opt -load-pass-plugin=./MaxLoopDepth.so \
-passes="max-loop-depth" nested_loops.ll -disable-output
输出:
循环 at for.cond (深度 1) [含 1 个子循环]
循环 at for.cond1 (深度 2) [含 1 个子循环]
循环 at for.cond4 (深度 3)
=== matmul ===
循环总数: 3
最大嵌套深度: 3
循环 at for.cond (深度 1)
=== simple ===
循环总数: 1
最大嵌套深度: 1
关键 API 说明:
// 获取 LoopInfo
auto &LI = FAM.getResult<LoopAnalysis>(F);
// 遍历所有循环(前序:外→内)
for (Loop *L : LI.getLoopsInPreorder()) { ... }
// 获取顶层循环(没有外层循环的)
for (Loop *L : LI) { ... } // LI 本身是一个顶层循环的容器
// 循环信息
L->getHeader(); // 循环头 BasicBlock*
L->getLoopDepth(); // 嵌套深度(顶层=1)
L->getSubLoops(); // 子循环列表
L->getExitingBlock(); // 循环出口块
L->getLoopLatch(); // latch 块
L->getLoopPreheader(); // preheader 块(需要 LoopSimplify 先跑)
L->isLoopSimplifyForm(); // 是否已被规范化
练习 3:用 TableGen 定义一条指令(15 分钟)
创建一个最小的 .td 文件,定义一条自定义指令:
// mini.td —— 最小的 TableGen 文件
def R0 : Register<"r0">;
def GR8 : RegisterClass<"MyTarget", [i8], 8, (add R0)>;
def ADDi8 : Instruction {
let OutOperandList = (outs GR8:$rd);
let InOperandList = (ins GR8:$rs, i8imm:$imm);
let AsmString = "add $rd, $rs, $imm";
let Pattern = [(set i8:$rd, (add i8:$rs, imm:$imm))];
}
用 llvm-tblgen 处理:
llvm-tblgen mini.td -print-records
▼ 练习 3 参考答案——运行结果与解读
完整可运行的 mini.td 文件:
// mini.td —— 完整的最小 TableGen 文件
// 如果要在 LLVM build 目录下直接运行,需要 include 基础定义
// ---- 第1步:定义寄存器 ----
def R0 : Register<"r0">;
def R1 : Register<"r1">;
// ---- 第2步:定义寄存器类(一类寄存器的集合) ----
// 参数: 目标名称, 可存类型列表, 对齐(字节), 寄存器列表
def GR8 : RegisterClass<"MyTarget", [i8], 8, (add R0, R1)>;
// ---- 第3步:定义指令 ----
def ADDi8 : Instruction {
let OutOperandList = (outs GR8:$rd); // 输出:目标寄存器
let InOperandList = (ins GR8:$rs, i8imm:$imm); // 输入:源寄存器+8位立即数
let AsmString = "add $rd, $rs, $imm"; // 汇编格式
let Pattern = [(set i8:$rd, (add i8:$rs, imm:$imm))]; // IR匹配模式
}
// ---- 第4步:再定义一条减法指令,演示比较 ----
def SUBi8 : Instruction {
let OutOperandList = (outs GR8:$rd);
let InOperandList = (ins GR8:$rs, i8imm:$imm);
let AsmString = "sub $rd, $rs, $imm";
let Pattern = [(set i8:$rd, (sub i8:$rs, imm:$imm))];
}
运行 TableGen:
# 最简单的用法:看 TableGen 怎么解析
llvm-tblgen mini.td -print-records
# 如果需要 include 基础定义(大多数实际 .td 文件):
llvm-tblgen mini.td -I /path/to/llvm/include -print-records
-print-records 输出(精简解读):
------------- Classes -----------------
class Instruction {
...
}
class Register<string n> {
string AsmName = n;
...
}
------------- Defs -----------------
def ADDi8 { // Instruction 的实例
dag OutOperandList = (outs GR8:$rd);
dag InOperandList = (ins GR8:$rs, i8imm:$imm);
string AsmString = "add $rd, $rs, $imm";
dag Pattern = (set (i8 GR8:$rd), (add (i8 GR8:$rs), (imm i8imm:$imm)));
}
def R0 { // Register 的实例
string AsmName = "r0";
}
...
TableGen 类型和关键概念解读:
| TableGen 概念 | 对应的 C++ 含义 |
|---|---|
dag | 有向无环图——指令选择的 IR 模式。(set dst, (add src, imm)) |
bits<n> | 位字段,用于编码操作码/寄存器号 |
RegisterClass | 一组可互换的寄存器——指令的操作数类型 |
Pattern | 指令选择器用来匹配 IR 的"模板"。告诉编译器"遇到这个 IR 模式就用这条指令" |
Operand | 操作数类型定义——寄存器、立即数、内存地址等 |
Pattern 的 DAG 语法详解:
let Pattern = [(set i8:$rd, (add i8:$rs, imm:$imm))];
// └────────┬──────┘
// 这是一个 dag 表达式
//
// 解读:
// (set 目标, 操作) — set 是最外层,表示"赋值"操作
// 目标: i8:$rd — 赋值目标是 $rd(类型 i8)
// 操作: (add i8:$rs, imm:$imm) — IR add 操作,两个操作数是 $rs 和 $imm
//
// 这条 Pattern 匹配的 IR:
// %rd = add i8 %rs, 立即数
//
// 匹配后,指令选择器输出:
// add %rd, %rs, <立即数>
生成不同输出:
# 生成指令信息(编码、操作数等)
llvm-tblgen mini.td -I llvm/include -gen-instr-info
# 生成寄存器信息
llvm-tblgen mini.td -I llvm/include -gen-register-info
# 生成指令选择匹配表(实际用 TableGen 后端 -gen-dag-isel)
llvm-tblgen mini.td -I llvm/include -gen-dag-isel
# 列出所有可用的生成器
llvm-tblgen -help-list
常见输出:
-gen-instr-info→MCInstrDesc数组(指令描述)-gen-register-info→MCRegisterInfo(寄存器信息)-gen-dag-isel→ 指令选择匹配表(NDFA 自动机)-gen-asm-matcher→ 汇编解析器匹配表-gen-disassembler→ 反汇编表-gen-callingconv→ 调用约定实现
如果遇到 include 依赖问题,创建一个自给自足的版本:
// standalone.td —— 完全独立的 TableGen 文件,无需 include
// 手动定义所有需要的基类
class Register<string name> {
string AsmName = name;
}
class RegisterClass<string namespace, list<ValueType> regTypes,
int align, dag regList> {
string Namespace = namespace;
list<ValueType> RegTypes = regTypes;
int Alignment = align;
dag MemberList = regList;
}
class Instruction {
dag OutOperandList;
dag InOperandList;
string AsmString = "";
dag Pattern;
bit isTerminator = 0;
bit isBranch = 0;
}
// 操作符定义(用于 Pattern DAG)
def set; // set 操作
def add; // add 操作
def sub; // sub 操作
// 值类型
def i8 : ValueType<8>;
def i16 : ValueType<16>;
def i32 : ValueType<32>;
// 立即数操作数
def i8imm : Operand<i8>;
def i16imm : Operand<i16>;
def imm : Operand<i32>; // 通用立即数(用于匹配模式)
// ---- 自定义目标开始 ----
def R0 : Register<"r0">;
def R1 : Register<"r1">;
def R2 : Register<"r2">;
def R3 : Register<"r3">;
def GR8 : RegisterClass<"MyTarget", [i8], 8, (add R0, R1, R2, R3)>;
def ADDi8 : Instruction {
let OutOperandList = (outs GR8:$rd);
let InOperandList = (ins GR8:$rs, i8imm:$imm);
let AsmString = "add $rd, $rs, $imm";
let Pattern = [(set i8:$rd, (add i8:$rs, imm:$imm))];
}
这个独立版本可以直接用 llvm-tblgen standalone.td -print-records 运行。
今日小结
上午(龙书理论):
三地址码: 表达式 AST 后序遍历生成 TAC
短路求值: true/false 标签 + 回填
GCSE: 可用表达式分析 → 消除重复计算
PRE: 部分冗余 → 代码提升 + 插入 + 删除
循环优化: LICM / 归纳变量 / 展开
下午(LLVM 实战):
TableGen: 声明式描述目标架构 → 自动生成 C++
Pass 体系: mem2reg → GVN → LICM → indvars → loop-unroll/vectorize → DCE → simplifycfg
写 Pass: InstCount(分析)/ AddToSub(变换)
IRBuilder: 创建/替换/删除指令的 API
关键认知:
-
优化 = 数据流分析 + 重写规则。每个 Pass 都有明确的数学基础(GVN=可用表达式,LICM=循环不变分析,DCE=活跃变量),不是"聪明地猜到"优化机会。
-
Pass 管道是分层的。先 mem2reg(建立 SSA)→ 再做 GVN/LICM(依赖 SSA 的简洁性)→最后 DCE(清理)。顺序错了效果就出不来。
-
写 Pass 是理解 LLVM 的最佳途径。当你需要遍历 CFG、插入指令、替换 use 时,你自然就深入了 LLVM 的 IR 数据结构。
明天:把这些优化后的 IR 变成真正的机器码——指令选择、寄存器分配、指令调度。

783

被折叠的 条评论
为什么被折叠?



