mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
1770 字
5 分钟
中间表示:IR 与 SSA
2025-12-31

中间表示(IR)是编译器的核心数据结构——它是前端和后端之间的桥梁,也是所有优化的作用对象。一个好的 IR 设计决定了编译器的优化能力、可扩展性和代码质量。

IR 是这一章的重点——为什么需要 SSA?如何构造 SSA?基本块和控制流图如何组织?LLVM IR 长什么样?

一、IR 的作用与设计#

1.1 为什么需要 IR?#

flowchart LR subgraph 没有IR["没有 IR:M×N 问题"] C1["C → x86"] C2["C → ARM"] C3["C → RISC-V"] R1["Rust → x86"] R2["Rust → ARM"] end subgraph 有IR["有 IR:M+N 问题"] CF["C 前端"] --> IR["统一 IR"] RF["Rust 前端"] --> IR IR --> X86["x86 后端"] IR --> ARM["ARM 后端"] end style 没有IR fill:#fce4ec,stroke:#c62828 style 有IR fill:#e8f5e9,stroke:#2e7d32

IR 的核心作用:

  1. 解耦:前端和后端独立演化,通过 IR 连接
  2. 优化:提供一个平台无关的优化空间
  3. 验证:IR 可以被验证(类型检查、SSA 性质等)
  4. 分析:控制流分析、数据流分析都基于 IR

1.2 IR 的层次#

编译器通常使用多层 IR,从高层到低层逐步降低抽象级别:

flowchart TB HIR["HIR — 高层 IR<br/>接近源语言<br/>AST 线性化"] --> MIR["MIR — 中层 IR<br/>SSA 形式<br/>优化主要作用层"] MIR --> LIR["LIR — 低层 IR<br/>接近机器码<br/>指令选择输入"] style HIR fill:#e3f2fd,stroke:#1565c0 style MIR fill:#fff3e0,stroke:#e65100 style LIR fill:#e8f5e9,stroke:#2e7d32
IR 层次抽象级别特点典型代表
HIR保留高级结构(循环、switch)Rust HIR、Swift SIL
MIRSSA 形式,三地址码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 是优化的基础?#

graph TB SSA["SSA 性质"] --> BEN1["def-use 链<br/>直接找到使用点"] SSA --> BEN2["值编号<br/>相同计算→相同值"] SSA --> BEN3["常量传播<br/>定义处已知→使用处已知"] SSA --> BEN4["死代码消除<br/>无使用→可删除"] SSA --> BEN5["稀疏条件常量传播<br/>SCCP"] style SSA fill:#e3f2fd,stroke:#1565c0
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 φ 函数的语义#

φ 函数不是真正的运行时操作——它是一个编译期概念,表示”根据控制流来源选择值”:

flowchart TB COND["x > 0?"] COND -->|true| THEN["a1 = x + y"] COND -->|false| ELSE["a2 = x - y"] THEN --> MERGE["a3 = φ(a1, a2)"] ELSE --> MERGE MERGE --> RET["return a3"] style COND fill:#e3f2fd,stroke:#1565c0 style THEN fill:#e8f5e9,stroke:#2e7d32 style ELSE fill:#fff3e0,stroke:#e65100 style MERGE fill:#fce4ec,stroke:#c62828
Note

φ 函数在代码生成阶段会被消除——编译器在基本块末尾插入”并行复制”操作,将 φ 函数的参数复制到目标寄存器。这个”并行”很重要——所有复制必须同时发生,否则可能覆盖值。

三、基本块与控制流图#

3.1 基本块的定义#

基本块是最大的连续指令序列,满足:

  1. 只有第一条指令可以作为入口
  2. 只有最后一条指令可以作为出口
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 cfg

3.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 dom

3.4 支配树与支配边界#

flowchart TB ENTRY["Entry"] --> A["A"] ENTRY --> B["B"] A --> C["C"] A --> D["D"] B --> D2["D"] C --> E["E"] D2 --> E2["E"] style ENTRY fill:#e3f2fd,stroke:#1565c0

支配树(上图中 Entry 是根,每个节点的父节点是其直接支配者):

节点直接支配者支配边界
Entry
AEntryD
BEntryD
CAE
DEntryE
EA
Warning

支配边界是 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, invokebr 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 IR
cat > example.c << 'EOF'
int sum(int n) {
int result = 0;
for (int i = 0; i < n; i++) {
result += i;
}
return result;
}
EOF
# 未优化的 IR
clang -emit-llvm -S -O0 example.c -o example_O0.ll
# 优化后的 IR
clang -emit-llvm -S -O2 example.c -o example_O2.ll
# 对比
diff example_O0.ll example_O2.ll
# 使用 opt 运行特定优化 Pass
opt -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.label

6.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类型化层次
LLVMLLVM IR单层(多 Pass)
GCCGIMPLE → RTLGIMPLE 是GIMPLE 是两层
GoSSA (自研)单层
RustHIR → MIR → LLVM IRMIR 是三层
V8Sea of Nodes图 IR
Java字节码单层
Tip

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 = entry
cfg.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 IR
cat > 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.ll
cat fib.ll
# 使用 opt 优化
opt -passes=mem2reg,sccp,adce fib.ll -S -o fib_opt.ll
cat fib_opt.ll

8.3 实验三:查看 Go 的 SSA#

cat > hello.go << 'EOF'
package main
func 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 IRSSA 形式、显式类型、三地址码、无限寄存器

以上就是IR 与 SSA的核心内容。下一章进入 优化基础,看看编译器如何在 SSA 形式的 IR 上做常量折叠、死代码消除等基础优化。

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

中间表示:IR 与 SSA
https://blog.souloss.com/posts/compiler/intermediate-representation/
作者
Souloss
发布于
2025-12-31
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时