中间表示(IR)是编译器的核心数据结构——它是前端和后端之间的桥梁,也是所有优化的作用对象。一个好的 IR 设计决定了编译器的优化能力、可扩展性和代码质量。
IR 是这一章的重点——为什么需要 SSA?如何构造 SSA?基本块和控制流图如何组织?LLVM IR 长什么样?
一、IR 的作用与设计
1.1 为什么需要 IR?
IR 的核心作用:
- 解耦:前端和后端独立演化,通过 IR 连接
- 优化:提供一个平台无关的优化空间
- 验证:IR 可以被验证(类型检查、SSA 性质等)
- 分析:控制流分析、数据流分析都基于 IR
1.2 IR 的层次
编译器通常使用多层 IR,从高层到低层逐步降低抽象级别:
| IR 层次 | 抽象级别 | 特点 | 典型代表 |
|---|---|---|---|
| HIR | 高 | 保留高级结构(循环、switch) | Rust HIR、Swift SIL |
| MIR | 中 | SSA 形式,三地址码 | LLVM IR、Go SSA |
| LIR | 低 | 接近机器指令,暴露寄存器 | LLVM MachineInstr、GCC RTL |
1.3 IR 设计的权衡
| 设计选择 | 方案 A | 方案 B | 影响 |
|---|---|---|---|
| 操作数位置 | 三地址码(最多两个源操作数) | 多地址码(任意数量) | 三地址码更适合优化 |
| 值的命名 | 虚拟寄存器(无限) | SSA(每个值唯一定义) | SSA 使优化更简单 |
| 控制流 | 显式跳转 | 结构化控制流 | 显式跳转更灵活 |
| 类型信息 | 有类型 IR | 无类型 IR | 有类型 IR 更安全 |
| 内存模型 | 显式 load/store | 隐式内存操作 | 显式更利于优化 |
二、SSA:静态单赋值形式
2.1 SSA 的定义
SSA(Static Single Assignment)要求每个变量只被赋值一次。如果同一个变量在多个分支中被赋值,在汇合点使用 φ(phi)函数选择正确的值:
// 非 SSA 形式int foo(int x, int y) { int a; if (x > 0) { a = x + y; // a 的第一次赋值 } else { a = x - y; // a 的第二次赋值 } return a; // 使用 a —— 哪个值?}
// SSA 形式int foo_ssa(int x, int y) { int a1, a2, a3; if (x > 0) { a1 = x + y; } else { a2 = x - y; } a3 = phi(a1, a2); // phi 函数:根据控制流选择 a1 或 a2 return a3;}2.2 为什么 SSA 是优化的基础?
| SSA 带来的好处 | 说明 |
|---|---|
| 简化的数据流分析 | 每个变量只有一个定义,use-def 链是唯一的 |
| 值编号 | 如果两个操作数相同,结果一定相同 |
| 更简单的优化实现 | 不需要维护变量到定义的映射 |
| 更少的分析开销 | 稀疏分析(只遍历 def-use 链)而非密集分析 |
2.3 SSA 构造算法
SSA 构造的核心是插入 φ 函数和重命名变量:
def construct_ssa(cfg): """从非 SSA 的 CFG 构造 SSA 形式"""
# 第一步:计算支配关系 dom_tree = compute_dominators(cfg)
# 第二步:计算支配边界 dom_frontier = compute_dominance_frontier(dom_tree)
# 第三步:插入 phi 函数 insert_phi_functions(cfg, dom_frontier)
# 第四步:重命名变量 rename_variables(cfg, dom_tree)
return cfg
def compute_dominance_frontier(dom_tree): """计算支配边界:φ 函数的插入位置""" df = {} for node in dom_tree.nodes: df[node] = set() for predecessor in node.predecessors: runner = predecessor while runner != dom_tree.idom(node): df[runner].add(node) runner = dom_tree.idom(runner) return df
def insert_phi_functions(cfg, dom_frontier): """在支配边界处插入 φ 函数""" for var in cfg.all_variables: # 收集 var 的所有定义点 defs = [block for block in cfg.blocks if var in block.definitions]
# 在定义点的支配边界插入 φ 函数 worklist = list(defs) visited = set() while worklist: block = worklist.pop() for frontier_block in dom_frontier[block]: if frontier_block not in visited: visited.add(frontier_block) # 插入 φ 函数 phi = PhiFunction(var, len(frontier_block.predecessors)) frontier_block.prepend(phi) worklist.append(frontier_block)2.4 φ 函数的语义
φ 函数不是真正的运行时操作——它是一个编译期概念,表示”根据控制流来源选择值”:
φ 函数在代码生成阶段会被消除——编译器在基本块末尾插入”并行复制”操作,将 φ 函数的参数复制到目标寄存器。这个”并行”很重要——所有复制必须同时发生,否则可能覆盖值。
三、基本块与控制流图
3.1 基本块的定义
基本块是最大的连续指令序列,满足:
- 只有第一条指令可以作为入口
- 只有最后一条指令可以作为出口
class BasicBlock: """基本块""" def __init__(self, label): self.label = label self.instructions = [] # 指令列表 self.predecessors = [] # 前驱基本块 self.successors = [] # 后继基本块 self.dominates = set() # 支配的基本块 self.idom = None # 直接支配者
def is_terminated(self): """检查基本块是否以终止指令结尾""" if not self.instructions: return False last = self.instructions[-1] return last.op in ('br', 'ret', 'switch', 'unreachable')
class ControlFlowGraph: """控制流图""" def __init__(self): self.blocks = [] # 基本块列表 self.entry = None # 入口基本块
def add_block(self, block): self.blocks.append(block)
def add_edge(self, src, dst): src.successors.append(dst) dst.predecessors.append(src)3.2 控制流图的构建
def build_cfg_from_ir(instructions): """从 IR 指令序列构建控制流图""" cfg = ControlFlowGraph() current_block = BasicBlock("entry") cfg.entry = current_block cfg.add_block(current_block)
for inst in instructions: # 遇到标签,开始新的基本块 if inst.op == 'label': new_block = BasicBlock(inst.label) cfg.add_block(new_block) # 如果当前块没有终止指令,添加隐式跳转 if not current_block.is_terminated(): cfg.add_edge(current_block, new_block) current_block = new_block continue
current_block.instructions.append(inst)
# 遇到终止指令,当前基本块结束 if inst.op == 'br': # 条件跳转:两个后继 true_block = find_block(cfg, inst.true_label) false_block = find_block(cfg, inst.false_label) cfg.add_edge(current_block, true_block) cfg.add_edge(current_block, false_block) elif inst.op == 'jmp': # 无条件跳转:一个后继 target = find_block(cfg, inst.target) cfg.add_edge(current_block, target) elif inst.op == 'ret': pass # 返回,无后继
return cfg3.3 支配关系
支配(Dominator):如果从入口到 B 的每条路径都经过 A,则 A 支配 B(记作 A dom B)。
def compute_dominators(cfg): """计算支配关系(迭代数据流算法)""" entry = cfg.entry dom = {}
# 初始化:入口只被自己支配,其他被所有块支配 for block in cfg.blocks: if block == entry: dom[block] = {entry} else: dom[block] = set(cfg.blocks)
# 迭代直到不变 changed = True while changed: changed = False for block in cfg.blocks: if block == entry: continue # dom(B) = {B} ∪ ∩ dom(P) for all P in preds(B) new_dom = {block} if block.predecessors: pred_doms = [dom[p] for p in block.predecessors] intersection = set.intersection(*pred_doms) new_dom |= intersection
if new_dom != dom[block]: dom[block] = new_dom changed = True
return dom3.4 支配树与支配边界
支配树(上图中 Entry 是根,每个节点的父节点是其直接支配者):
| 节点 | 直接支配者 | 支配边界 |
|---|---|---|
| Entry | — | — |
| A | Entry | D |
| B | Entry | D |
| C | A | E |
| D | Entry | E |
| E | A | — |
支配边界是 SSA 构造中 φ 函数插入位置的关键。如果一个变量在块 A 中定义,那么 φ 函数需要插入到 A 的支配边界中的每个块。
四、LLVM IR 详解
4.1 LLVM IR 示例
; 一个简单的加法函数define i32 @add(i32 %a, i32 %b) {entry: %result = add i32 %a, %b ret i32 %result}
; 带条件分支的函数define i32 @abs(i32 %x) {entry: %cmp = icmp sgt i32 %x, 0 ; 有符号比较: x > 0 br i1 %cmp, label %pos, label %neg
pos: ret i32 %x
neg: %result = sub i32 0, %x ; 0 - x = -x ret i32 %result}
; 带 phi 函数的函数define i32 @max(i32 %a, i32 %b) {entry: %cmp = icmp sgt i32 %a, %b br i1 %cmp, label %then, label %else
then: br label %merge
else: br label %merge
merge: %result = phi i32 [ %a, %then ], [ %b, %else ] ret i32 %result}4.2 LLVM IR 的核心特性
| 特性 | 说明 |
|---|---|
| SSA 形式 | 每个虚拟寄存器只被赋值一次 |
| 显式类型 | 每个值都有类型(i32, f64, ptr 等) |
| 三地址码 | 每条指令最多两个源操作数 |
| 显式控制流 | 基本块 + 显式跳转 |
| 无限寄存器 | 虚拟寄存器数量无限制 |
| 内存操作显式 | load/store 显式操作内存 |
4.3 LLVM IR 指令分类
| 类别 | 指令 | 示例 |
|---|---|---|
| 算术 | add, sub, mul, sdiv, udiv | %r = add i32 %a, %b |
| 浮点 | fadd, fsub, fmul, fdiv | %r = fadd double %a, %b |
| 位运算 | and, or, xor, shl, lshr, ashr | %r = and i32 %a, %b |
| 比较 | icmp, fcmp | %r = icmp sgt i32 %a, %b |
| 转换 | trunc, zext, sext, fptoui, uitofp | %r = sext i8 %a to i32 |
| 内存 | alloca, load, store, getelementptr | %r = load i32, ptr %p |
| 控制 | br, ret, switch, invoke | br i1 %c, label %t, label %f |
| 聚合 | insertvalue, extractvalue | %r = insertvalue {i32, f64} %s, 42, 0 |
| 向量 | shufflevector, insertelement | %r = insertelement <4 x i32> %v, 42, i32 0 |
| 其他 | phi, call, select | %r = phi i32 [%a, %bb1], [%b, %bb2] |
4.4 用 Clang 生成和查看 LLVM IR
# 生成 LLVM IRcat > example.c << 'EOF'int sum(int n) { int result = 0; for (int i = 0; i < n; i++) { result += i; } return result;}EOF
# 未优化的 IRclang -emit-llvm -S -O0 example.c -o example_O0.ll
# 优化后的 IRclang -emit-llvm -S -O2 example.c -o example_O2.ll
# 对比diff example_O0.ll example_O2.ll
# 使用 opt 运行特定优化 Passopt -passes=mem2reg,adce example_O0.ll -S -o example_opt.ll五、IR 生成:从 AST 到 IR
5.1 IR 生成器
class IRGenerator(ASTVisitor): """从 AST 生成 SSA 形式的 IR"""
def __init__(self): self.instructions = [] self.counter = 0 self.current_block = BasicBlock("entry")
def new_temp(self): """生成新的临时变量名""" self.counter += 1 return f"%t{self.counter}"
def emit(self, instruction): """发射一条 IR 指令""" self.instructions.append(instruction) return instruction.result
def visit_NumberLit(self, node): temp = self.new_temp() self.emit(IRInst('const', temp, node.value)) return temp
def visit_BinaryOp(self, node): left = self.visit(node.left) right = self.visit(node.right) temp = self.new_temp() self.emit(IRInst(node.op, temp, left, right)) return temp
def visit_VarDecl(self, node): if node.init: value = self.visit(node.init) self.emit(IRInst('store', node.name, value))
def visit_Identifier(self, node): temp = self.new_temp() self.emit(IRInst('load', temp, node.name)) return temp
def visit_IfStmt(self, node): cond = self.visit(node.condition) then_label = f"then_{self.counter}" else_label = f"else_{self.counter}" merge_label = f"merge_{self.counter}"
self.emit(IRInst('br', None, cond, then_label, else_label))
self.emit(IRInst('label', then_label)) self.visit(node.then_branch) self.emit(IRInst('jmp', None, merge_label))
self.emit(IRInst('label', else_label)) if node.else_branch: self.visit(node.else_branch) self.emit(IRInst('jmp', None, merge_label))
self.emit(IRInst('label', merge_label))
def visit_ReturnStmt(self, node): value = self.visit(node.value) self.emit(IRInst('ret', None, value))六、IR 验证
6.1 SSA 性质验证
def verify_ssa(cfg): """验证 CFG 是否满足 SSA 性质""" definitions = {} # 变量 → 定义位置
for block in cfg.blocks: for inst in block.instructions: if inst.op == 'phi': continue # phi 函数例外 if inst.result in definitions: raise VerificationError( f"Variable {inst.result} defined multiple times: " f"first at {definitions[inst.result]}, " f"again at {block.label}" ) definitions[inst.result] = block.label6.2 类型检查验证
def verify_types(cfg): """验证 IR 的类型一致性""" types = {} # 变量 → 类型
for block in cfg.blocks: for inst in block.instructions: if inst.op == 'add': if types[inst.operands[0]] != types[inst.operands[1]]: raise VerificationError( f"Type mismatch in add: " f"{types[inst.operands[0]]} vs {types[inst.operands[1]]}" ) types[inst.result] = types[inst.operands[0]]七、不同编译器的 IR 对比
| 编译器 | IR 名称 | SSA | 类型化 | 层次 |
|---|---|---|---|---|
| LLVM | LLVM IR | 是 | 是 | 单层(多 Pass) |
| GCC | GIMPLE → RTL | GIMPLE 是 | GIMPLE 是 | 两层 |
| Go | SSA (自研) | 是 | 是 | 单层 |
| Rust | HIR → MIR → LLVM IR | MIR 是 | 是 | 三层 |
| V8 | Sea of Nodes | 是 | 否 | 图 IR |
| Java | 字节码 | 否 | 是 | 单层 |
V8 的 Sea of Nodes 是一种图 IR——它将所有操作表示为图中的节点,数据依赖和控制依赖都是边。这种表示使得优化可以更自由地重排操作,但实现更复杂。
八、动手实践
8.1 实验一:手写 SSA 构造
# 构造一个简单 CFG 的 SSA 形式cfg = ControlFlowGraph()entry = BasicBlock("entry")then_bb = BasicBlock("then")else_bb = BasicBlock("else")merge = BasicBlock("merge")
cfg.entry = entrycfg.add_edge(entry, then_bb)cfg.add_edge(entry, else_bb)cfg.add_edge(then_bb, merge)cfg.add_edge(else_bb, merge)
# 在 then 和 else 中定义变量 a# → 需要在 merge 中插入 phi 函数dom = compute_dominators(cfg)df = compute_dominance_frontier(dom)insert_phi_functions(cfg, df)8.2 实验二:探索 LLVM IR
# 生成并查看 LLVM IRcat > fib.c << 'EOF'int fib(int n) { if (n <= 1) return n; return fib(n-1) + fib(n-2);}EOF
clang -emit-llvm -S -O0 fib.c -o fib.llcat fib.ll
# 使用 opt 优化opt -passes=mem2reg,sccp,adce fib.ll -S -o fib_opt.llcat fib_opt.ll8.3 实验三:查看 Go 的 SSA
cat > hello.go << 'EOF'package mainfunc add(a, b int) int { return a + b }func main() { println(add(3, 4)) }EOF
# 查看 Go 编译器的 SSA 阶段GOSSAFUNC=add go build -o hello hello.go# 会在浏览器中打开 SSA 的各个优化阶段九、本章小结
在上一章中,语义分析为 AST 标注了类型信息、解析了名称引用,让程序具备了完整的语义。但 AST 仍然是树形结构,不适合直接做优化和代码生成——编译器需要一个更底层、更统一的表示。这个连接前端与后端的核心数据结构,就是中间表示(IR)。
| 概念 | 要点 |
|---|---|
| IR 作用 | 解耦前端后端、优化空间、验证、分析 |
| IR 层次 | HIR(高层)→ MIR(中层 SSA)→ LIR(低层) |
| SSA | 每个变量只赋值一次,φ 函数在控制流汇合点选择值 |
| SSA 构造 | 计算支配关系 → 支配边界 → 插入 φ 函数 → 重命名 |
| 基本块 | 最大连续指令序列,单入口单出口 |
| 控制流图 | 基本块 + 边,优化的基础数据结构 |
| 支配关系 | A dom B:所有从入口到 B 的路径都经过 A |
| LLVM IR | SSA 形式、显式类型、三地址码、无限寄存器 |
以上就是IR 与 SSA的核心内容。下一章进入 优化基础,看看编译器如何在 SSA 形式的 IR 上做常量折叠、死代码消除等基础优化。
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






