1. LLVM 生态与 IR 概览
一句话总结: LLVM 提供语言无关的中间表示与一整套优化、代码生成基础设施,让前端专注语言、后端专注机器。
LLVM 的定位是「编译器的编译器」:它把「把语言翻译成机器码」这件事里的公共部分抽出来——中间表示、优化器、指令选择、寄存器分配、汇编与链接——做成可复用组件。一个语言前端只要把源码降级成 LLVM IR,就能免费获得跨平台目标代码、通用优化管线、调试信息与链接器支持。Rust、Swift、Clang、Julia 都走这条路线。
源码 (Swift/Rust/...)
│ 前端: 词法/语法/语义 → 语言专有 IR
▼
LLVM IR (SSA, 三地址码风格, 语言无关)
│ 优化器: opt / Pass Manager
▼
优化后 IR
│ SelectionDAG / GlobalISel → 指令选择
▼
目标机器码 + 目标文件 (x86/ARM/RISC-V)
| 组件 | 工具 | 职责 |
|---|---|---|
| IR 生成 | llvm-gcc 前端、Clang | 源码 → LLVM IR |
| 优化 | opt、Pass Manager | 在 IR 上跑 pass 序列 |
| 汇编 | llc | IR → 目标汇编 |
| 链接 | lld、系统链接器 | 目标文件 → 可执行文件 |
LLVM IR 是显式的三地址码:每行指令形如 %tmp = add i32 %a, %b,寄存器(虚拟寄存器)以 % 为前缀,类型紧随其后。IR 同时保留人类可读的文本形式与紧凑的 bitcode 形式,文本形式用于调试与测试,bitcode 用于跨阶段传递。理解 IR 的静态单赋值(SSA)性质是使用 LLVM 的关键前提。
2. SSA 形式与 phi 节点
一句话总结: SSA 要求每个变量只赋值一次,合并路径时引入 phi 节点,这让数据流显式化并简化大量优化。
静态单赋值(Static Single Assignment, SSA)的核心规则:每个虚拟寄存器恰好被定义一次。这样任意使用点都能立刻回溯到唯一的定义点,数据流信息被编码进程序本身。分支汇合处(if-else 之后、循环头)同一个变量有不同的候选值,于是引入 phi(φ)节点:%x = phi i32 [ %a, %then ], [ %b, %else ],表示「来自哪条路径就取哪个值」。
; C: int f(int c) { int x; if (c) x = 1; else x = 2; return x; }
define i32 @f(i32 %c) {
entry:
%cmp = icmp ne i32 %c, 0
br i1 %cmp, label %then, label %else
then:
br label %merge
else:
br label %merge
merge:
%x = phi i32 [ 1, %then ], [ 2, %else ]
ret i32 %x
}
# 用 Python 对 SSA 形式做一次简单的到达定义分析:
# 在 SSA 中, 每个 use 的唯一 def 就是它的定义点, 无需数据流迭代。
def reaching_def(ir_instrs):
defs = {}
uses = []
for idx, instr in enumerate(ir_instrs):
if instr.startswith("%") and "=" in instr:
name = instr.split(" ")[0]
defs[name] = idx
elif "%" in instr:
uses.append((idx, [tok for tok in instr.split()
if tok.startswith("%")]))
return defs, uses
phi 节点让控制流图(CFG)上的数据流变成程序结构的一部分:优化器看到 %x 的定义就是唯一的 phi,不再需要维护庞大的 use-def 数据库。把普通程序转成 SSA 的过程叫 SSA 构造(支配树 + 支配边界),把 SSA 转回普通形式的过程叫 phi 消除(并行 copy 传播)。LLVM 对用户透明地处理这两步,前端只需产出合规的 SSA 即可。
3. IRBuilder 生成 IR
一句话总结: IRBuilder 是增量生成 IR 的插入点 API,前端一边遍历自己的 AST 一边往 builder 当前插入点追加指令。
LLVM 的 C++ API 里,IRBuilder 维护一个「当前插入点」——某条 IR 指令之前或之后。前端遍历 AST 时每翻译一个节点就调用 builder.CreateAdd(...) 等工厂方法,指令被自动追加到当前块,并返回指向结果的 Value*。这种流式风格让前端代码读起来像在逐句生成汇编,而插入点可以随时切换(例如进入新基本块时)。
// 用 LLVM C++ API 生成 add 函数: int add(int a, int b) { return a + b; }
llvm::LLVMContext ctx;
llvm::IRBuilder<> builder(ctx);
auto *module = new llvm::Module("demo", ctx);
auto *i32 = llvm::Type::getInt32Ty(ctx);
auto *fnType = llvm::FunctionType::get(i32, {i32, i32}, false);
auto *fn = llvm::Function::Create(
fnType, llvm::Function::ExternalLinkage, "add", module);
auto *entry = llvm::BasicBlock::Create(ctx, "entry", fn);
builder.SetInsertPoint(entry);
auto *a = fn->getArg(0);
auto *b = fn->getArg(1);
auto *sum = builder.CreateAdd(a, b, "sum");
builder.CreateRet(sum);
# 概念等价物: 一个极简的 IR 生成器, 记录指令序列
class IRBuilderMini:
def __init__(self):
self.instrs = []
def add(self, a, b):
self.instrs.append(f"%t = add i32 {a}, {b}")
return f"%t"
def ret(self, val):
self.instrs.append(f"ret i32 {val}")
builder = IRBuilderMini()
sum_ = builder.add("%a", "%b")
builder.ret(sum_)
print(builder.instrs)
IRBuilder 之上的高级封装是 llvm::IRBuilder 的命名参数、块级 helper 与 CreateBinOp 等重载;更复杂的调用约定、内联钩子则需要直接操作 Function、Module 与 Attributes。前端要牢记一条纪律:不要在 IR 生成层做优化,IR 优化交给 pass 管线,前端只负责语义正确、类型显式的直译。
4. 优化 Pass 管线
一句话总结: Pass 管线是按依赖与收益排序的优化器队列,Pass Manager 负责调度、复用分析与维护不变式。
LLVM 优化器由一个个 pass 组成:常量折叠、死代码消除、内联、循环展开、向量化、全局值编号等。opt -O2 实际上展开成一个精心排序的 pass 序列。Pass Manager 的职责是记录 pass 之间依赖(某 pass 需要哪些分析结果、会 invalidate 哪些结果)、合并可复用的分析(如支配树、活跃变量),并保证每个 pass 执行前后 IR 不变式(SSA 合法、类型正确)成立。
-O2 的部分真实序列 (新 PM):
SimplifyCFG → SROA → InstCombine → EarlyCSE
→ FunctionAttrs → Inline → ... → LoopRotate → LICM
→ LoopUnswitch → ... → GVN → SCCP → ... → DCE
# 示意: 一个迷你 pass 管线, 支持依赖声明与迭代至收敛
class PassManager:
def __init__(self):
self.passes = []
def add(self, name, run, needs=()):
self.passes.append((name, run, needs))
def run(self, ir):
# 实际 LLVM 会做依赖排序与分析缓存复用
for name, run, needs in self.passes:
print(f"running pass: {name}")
ir = run(ir)
return ir
def constant_fold(ir):
# 示例: 把 add i32 1, 2 折叠为 3
return ir.replace("%t = add i32 1, 2", "%t = 3")
pm = PassManager()
pm.add("constfold", constant_fold)
pm.add("dce", lambda ir: ir, needs=("constfold",))
现代 LLVM 的 Pass Manager 分模块、函数、循环三个层级,pass 可在任意层级注册。新 PM(-fexperimental-new-pass-manager 起)相比老 PM 的改进是可缓存分析更细粒度、pass 之间顺序更可复现。优化序列的设计本质是「先做低开销、高收益的清理,再做循环与全局优化,最后清理一次」,收敛条件由 pass 内部迭代与整体序列共同保证。
5. 目标代码生成
一句话总结: 后端把优化后的 IR 通过指令选择、寄存器分配与指令调度变成目标汇编,LLVM 默认用 SelectionDAG 与 GlobalISel 两条路线。
llc 把 LLVM IR 翻译成目标机器汇编。现代 LLVM 走两条指令选择路线:成熟的 SelectionDAG 与新一代 GlobalISel。两者都把 IR 语义转换成目标指令,区别在于 GlobalISel 用更规则的匹配与更可预测的编译时间,正在逐步成为默认。指令选择之后是寄存器分配(图着色 / 线性扫描),把无限虚拟寄存器映射到有限物理寄存器,必要时溢出(spill)到栈上。
; 输入 IR
define i32 @scale(i32 %x) {
%m = mul i32 %x, 3
%s = add i32 %m, 1
ret i32 %s
}
; x86-64 输出 (节选, 优化开启)
scale:
leal (%rdi,%rdi,2), %eax ; x*3 用 LEA 一次完成
addl $1, %eax
ret
# 寄存器分配的活跃区间示例: 简单线性扫描
intervals = [ # (变量, 起始指令, 结束指令)
("%x", 0, 3), ("%m", 1, 2), ("%s", 2, 3),
]
def linear_scan(intervals, regs=2):
active, reg_map = [], {}
for name, start, end in sorted(intervals, key=lambda i: i[1]):
active = [iv for iv in active if iv[2] > start]
if len(active) >= regs:
spill = max(active, key=lambda iv: iv[2])
reg_map[spill[0]] = "spill"
active.remove(spill)
reg = regs - len(active)
reg_map[name] = f"r{reg}"
active.append((name, start, end))
return reg_map
目标代码生成还必须处理目标特有的细节:ABI 参数传递规则、栈帧布局、指令调度(重排指令利用流水线)、窥孔优化(指令序列局部替换)以及内建函数(如 x86 的 pclmulqdq、rdtsc)。一旦 IR 层与目标层分离,新增一个架构主要是新增一个 TargetMachine 后端,而语言前端完全不用改动。
6. 与前端 IR 的桥接
一句话总结: 前端先产出带语言语义的高级 IR,再降级为 LLVM IR,桥接层负责消除语言专属概念与类型。
LLVM IR 是语言无关的,但语言有自己的类型与语义——Python 的动态对象、Rust 的借用检查、Swift 的存在类型都不能直接映射。因此前端通常先构造语言专有的高层 IR(HIR),再写一个降级(lowering)模块把 HIR 翻译成 LLVM IR。桥接层的核心工作有三件:类型映射、所有权与引用语义的翻译、以及特殊运行时时序的安插。
# 前端 HIR 节点 → LLVM IR 的桥接分派示意
class Lowerer:
def lower(self, node):
match node:
case IntLit(v):
return self.builder.const_i32(v)
case VarRef(name):
return self.builder.load(self.env[name])
case Add(a, b):
l = self.lower(a)
r = self.lower(b)
return self.builder.add(l, r)
case Call(f, args):
fn = self.lower(f)
params = [self.lower(a) for a in args]
return self.builder.call(fn, params)
| 语言概念 | 降级策略 |
|---|---|
| 栈对象/结构体 | 内存分配 + GEP 访问 |
| 字符串 | 指针 + 长度结构或 i8* + strlen |
| 异常 | landingpad + invoke + personality |
| 闭包 | 环境结构体打包 + 函数指针 |
| GC 对象 | 内建 GC root 接口 + 写屏障 |
桥接层还要负责处理不可直接翻译的结构:如 Python 的属性查找要降级成字典查表 + 方法分派,Rust 的 trait 对象要降级成 vtable 指针。这一层最容易出错的地方是生命周期:LLVM IR 没有 GC 概念,前端必须在分配与回收点显式生成对应调用。设计良好的 HIR 能让桥接层保持机械翻译(mechanical translation),把语义决策留在 HIR 阶段。
7. 调试信息与内联汇编
一句话总结: 调试信息把 IR 指令映射回源码行列与变量,内联汇编则让前端在 IR 无法表达时直插目标指令。
只有 IR 不足以支撑良好的调试体验:调试器需要知道「这条指令来自源码哪一行」「这个寄存器对应哪个变量」。LLVM 通过 debug metadata 附加到 IR 指令上,-g 选项让 llc 把这些信息编码进 DWARF。前端生成 IR 时必须同步产出 DILocation、DILocalVariable 等 metadata,否则用户只能在没有符号的汇编上调试。
; 带调试信息的 IR (示意)
define i32 @f(i32 %x) !dbg !7 {
entry:
%y = add i32 %x, 1, !dbg !8
ret i32 %y, !dbg !9
}
!llvm.dbg.cu = !{!0}
!0 = distinct !DICompileUnit(language: DW_LANG_C, file: !1)
!7 = distinct !DISubprogram(name: "f", file: !1, line: 1)
!8 = !DILocation(line: 3, column: 5, scope: !7)
# 生成 debug 元数据是"每指令带位置"的簿记工作
class DebugEmitter:
def __init__(self):
self.line_map = {}
def record(self, ir_instr, source_line):
self.line_map[ir_instr] = source_line
def emit_dwarf(self, out):
for instr, line in self.line_map.items():
out.write(f"{instr} -> source line {line}\n")
内联汇编让前端绕过 IR 直写目标指令,常见于访问特殊寄存器、利用 CPU 原子指令或接入内建函数。LLVM 用 asm 语法把内联汇编块嵌入函数,并提供约束字符串描述操作数绑定。这是后端的逃生舱口:正常情况下前端应当避免依赖内联汇编,因为会丧失可移植性与优化机会,仅在标准库或平台接口处使用。
8. 总结
一句话总结: LLVM 后端把「从 IR 到机器码」的庞大工程收敛为可复用组件,前端只需要产出合规 SSA 并降级语言语义。
| 主题 | 核心结论 |
|---|---|
| 生态定位 | 语言无关 IR + 优化器 + 后端,前端专注语言 |
| SSA 形式 | 单次赋值 + phi 节点,数据流结构显式化 |
| IRBuilder | 插入点增量生成,流式翻译 AST |
| Pass 管线 | Pass Manager 调度依赖与分析缓存,分层迭代 |
| 目标生成 | SelectionDAG/GlobalISel 指令选择 + 寄存器分配 |
| 前端桥接 | HIR 降级为 IR,映射类型、闭包、异常与 GC |
| 调试与汇编 | debug metadata 映射行列,内联汇编作逃生口 |
选择 LLVM 后端意味着用「接入成本」交换「全平台能力」:前端要承担 HIR 设计、类型映射与调试信息的生成,但换来的是一流的优化器、成熟的指令后端与庞大的生态工具链。理解 LLVM IR 的 SSA 本质、IRBuilder 的生成节奏与 Pass Manager 的调度模型,是任何语言接入 LLVM 的第一步,也是读懂 rustc、swiftc 与 clang 后端日志的基础。
延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。