一、引言:为什么需要 SSA
在编译器后端的世界里,中间表示(IR)的选择直接决定了优化的上限。传统的三地址码虽然直观,但变量可以被多次赋值,这使得数据流分析变得复杂——你必须追踪每一个变量在每一个程序点上可能的值。
Static Single Assignment(SSA)形式提出了一个约束:每个变量只能被赋值恰好一次。这个简单的不变式带来了深远的影响:
- def-use chain 天然唯一——每个使用点只对应一个定义点;
- 大量经典优化在 SSA 上变得更高效;
- 寄存器分配可以利用 SSA 的性质简化冲突图构建。
SSA 并非学术界的玩具。LLVM IR 就是 SSA 形式,GCC 的 GIMPLE 也在 Tree SSA 阶段使用 SSA,Java HotSpot 的 C2 编译器使用 Sea of Nodes(一种 SSA 变体),V8 的 TurboFan 同样如此。可以说,不理解 SSA 就无法理解现代编译器。
本文将从 SSA 的定义出发,深入支配树构造、φ 函数放置、经典优化算法,最后给出完整的 Python 实现。
二、SSA 的基本定义
2.1 形式化定义
SSA 形式要求程序中每个变量在静态意义上只有一个赋值语句。运行时赋值可能执行多次(循环中),但源代码文本中只出现一次。
原始代码: SSA 形式:
x = 1 x_1 = 1
x = 2 x_2 = 2
y = x y_1 = x_2
2.2 φ 函数
当控制流汇合时,一个变量可能来自不同的分支,需要引入 φ(phi)函数:
if (cond):
x_1 = 1
else:
x_2 = 2
x_3 = φ(x_1, x_2) // 根据控制流来源选择值
y_1 = x_3
φ 函数是伪指令,它在基本块入口处根据控制流来源选择对应的值,由编译器在后续阶段消除。
2.3 为什么 SSA 简化优化
| 特性 | 传统 IR | SSA 形式 |
|---|---|---|
| 变量定义次数 | 任意多次 | 恰好一次 |
| def-use chain | 需要数据流分析 | 直接从变量名获取 |
| 常量传播 | 迭代不动点 | 单遍扫描(SCCP) |
| 死代码消除 | 需要活跃变量分析 | 检查 use 集合是否为空 |
| 公共子表达式 | 需要可用表达式分析 | GVN 直接比较 |
核心优势:由于每个变量只有一个定义,使用 -> 定义的关系是直接的——只需查看变量名即可。许多优化从 O(n^2) 降低到 O(n)。
三、支配关系与支配树
要构造 SSA,首先需要理解支配关系——φ 函数的放置位置由支配边界决定。
3.1 支配关系的定义
给定 CFG 中的入口节点 Entry,节点
d 支配 节点
n(记作 d dom n),当且仅当从
Entry 到 n
的每一条路径都经过 d。
基本性质:
- 自反性:每个节点支配自身;
- 传递性:
a dom b且b dom c,则a dom c; - 反对称性:
a dom b且b dom a,则a = b。
严格支配:d sdom n 当且仅当
d dom n 且 d ≠ n。
3.2 立即支配者(idom)
节点 n 的立即支配者 idom(n)
是严格支配 n 的节点中最接近 n
的那个:
idom(n) = d,其中 d sdom n,且不存在 d' 使得 d sdom d' sdom n
以 idom
关系构成的树称为支配树,入口节点为根。
CFG: 支配树:
Entry Entry
/ \ / | \
B1 B2 B1 B2 B5
| | | | |
B3 B4 B3 B4 B6
\ / |
B5 Exit
|
B6
|
Exit
B1支配B3(到B3的唯一路径必经B1);B1不支配B5(可从Entry → B2 → B4 → B5绕过B1);idom(B5) = Entry。
四、Lengauer-Tarjan 算法
4.1 算法概述
计算支配树的朴素方法需要 O(V^2) 甚至 O(V^3) 时间。Lengauer-Tarjan(1979)算法的时间复杂度为 O(E·α(V)),α 是反 Ackermann 函数,实际可视为常数。
核心步骤:
- DFS 编号:对 CFG 进行深度优先搜索,分配 DFS 序号;
- 半支配者(semidominator):利用 DFS 树路径计算;
- 从 sdom 推导 idom:利用定理从半支配者推导立即支配者。
4.2 半支配者
sdom(w) = min{ v | 存在路径 v = v_0, v_1, ..., v_k = w,
其中对所有 1 ≤ i ≤ k-1,dfn(v_i) > dfn(w) }
sdom(w) 是能通过 DFS 编号大于 w
的中间节点到达 w 的最小 DFS 编号节点。
4.3 从 sdom 到 idom
定理 1:对于 DFS 树中
sdom(w) 到 w 路径上(不含
sdom(w))的所有节点 u,如果
sdom(u) ≥ sdom(w),则
idom(w) = sdom(w)。
定理 2:否则,设 u
是该路径上 sdom(u) 最小的节点,则
idom(w) = idom(u)。
4.4 Link-Eval 数据结构
算法需要高效回答查询:给定路径上的节点,找到
sdom
值最小的那个。通过带路径压缩的并查集变体实现:
class LinkEval:
"""带路径压缩的 Link-Eval 数据结构"""
def __init__(self, n):
self.ancestor = [0] * n
self.label = list(range(n))
self.semi = list(range(n))
def find(self, v):
if self.ancestor[v] == 0:
return v
u = self.find(self.ancestor[v])
if self.semi[self.label[self.ancestor[v]]] < self.semi[self.label[v]]:
self.label[v] = self.label[self.ancestor[v]]
self.ancestor[v] = self.ancestor[self.ancestor[v]]
return self.label[v]
def link(self, v, w):
self.ancestor[w] = v4.5 完整算法
def lengauer_tarjan(cfg, entry):
"""Lengauer-Tarjan 支配树算法,O(E·α(V))"""
dfn = {}
vertex = []
parent = {}
pred = {v: [] for v in cfg}
counter = [0]
def dfs(v):
dfn[v] = counter[0]
vertex.append(v)
counter[0] += 1
for w in cfg.get(v, []):
pred[w].append(v)
if w not in dfn:
parent[w] = v
dfs(w)
dfs(entry)
n = counter[0]
semi = list(range(n))
idom = [0] * n
ancestor = [0] * n
label = list(range(n))
bucket = [[] for _ in range(n)]
def find(v):
if ancestor[v] == 0:
return v
u = find(ancestor[v])
if semi[label[ancestor[v]]] < semi[label[v]]:
label[v] = label[ancestor[v]]
ancestor[v] = ancestor[ancestor[v]]
return label[v]
# 按 DFS 逆序计算 sdom
for i in range(n - 1, 0, -1):
w = vertex[i]
for v in pred.get(w, []):
if v not in dfn:
continue
u = find(dfn[v])
if semi[u] < semi[i]:
semi[i] = semi[u]
bucket[semi[i]].append(i)
ancestor[i] = dfn[parent[w]]
p = dfn[parent[w]]
for v in bucket[p]:
u = find(v)
idom[v] = p if semi[u] == semi[v] else u
bucket[p].clear()
# 修正 idom
for i in range(1, n):
if idom[i] != semi[i]:
idom[i] = idom[idom[i]]
return {vertex[i]: vertex[idom[i]] for i in range(1, n)}五、支配边界与 φ 函数放置
5.1 支配边界的定义
DF(n) = { y | 存在 x → y 是 CFG 中的边,n dom x 且 n 不严格支配 y }
DF(n) 中的节点是”从 n
的支配区域刚好走出去”的汇合点——n 能控制到达
y 的某条路径,但不能控制所有路径。
对于前面的例子: -
DF(B1) = {B5}:B1 支配
B3,B3 → B5 是边,但
B1 不支配 B5; -
DF(B6) = {B2}:B6 → B2
是回边,B6 不支配 B2。
5.2 高效计算
def compute_df(cfg, idom):
"""O(E) 时间计算支配边界"""
df = {b: set() for b in cfg}
preds = {b: [] for b in cfg}
for v in cfg:
for w in cfg[v]:
preds[w].append(v)
for y in cfg:
if len(preds[y]) < 2:
continue
for x in preds[y]:
runner = x
while runner is not None and runner != idom.get(y):
df[runner].add(y)
runner = idom.get(runner)
return df5.3 Cytron 算法:φ 函数放置
Cytron
等人(1991)的算法基于迭代支配边界(IDF):对每个变量
v,φ 函数放置在 IDF(Defs(v))
中。
def place_phi(cfg, df, var_defs):
"""Cytron 算法放置 φ 函数"""
phi = {v: set() for v in var_defs}
for v, def_blocks in var_defs.items():
worklist = list(def_blocks)
visited = set(def_blocks)
while worklist:
block = worklist.pop()
for y in df.get(block, set()):
if y not in phi[v]:
phi[v].add(y)
if y not in visited:
visited.add(y)
worklist.append(y)
return phi5.4 Pruned SSA
Cytron 算法放置的 φ 函数中,很多对应的变量在该块之后并未被使用。Pruned SSA 通过预计算活跃性信息,只在变量活跃的汇合点放置 φ 函数,通常减少 30%~60%。
Semi-pruned SSA 是折中方案:只排除单基本块内使用的局部变量的 φ 函数。
def place_pruned_phi(cfg, df, var_defs, live_in):
"""Pruned SSA:只在变量活跃的位置放置 φ 函数"""
phi = {v: set() for v in var_defs}
for v, def_blocks in var_defs.items():
worklist = list(def_blocks)
visited = set(def_blocks)
while worklist:
block = worklist.pop()
for y in df.get(block, set()):
if y not in phi[v] and v in live_in.get(y, set()):
phi[v].add(y)
if y not in visited:
visited.add(y)
worklist.append(y)
return phi六、SSA 的完整构造
6.1 变量重命名
φ 函数放置完成后,通过支配树的前序遍历将变量替换为带版本号的名称:
def rename(cfg, dom_tree, phi_blocks, entry):
"""SSA 变量重命名(支配树前序遍历)"""
counter = {}
stack = {}
def new_name(var):
i = counter.get(var, 0)
counter[var] = i + 1
stack.setdefault(var, []).append(i)
return f"{var}.{i}"
def cur(var):
s = stack.get(var, [])
return f"{var}.{s[-1]}" if s else f"{var}.undef"
def visit(block):
saved = {v: len(stack.get(v, [])) for v in counter}
# φ 函数定义
for var in phi_blocks.get(block, []):
new_name(var)
# 普通指令:先重命名 use,再重命名 def
for instr in get_instrs(block):
instr.uses = [cur(u) for u in instr.uses]
if instr.dst:
instr.dst = new_name(instr.dst)
# 填充后继的 φ 参数
for succ in cfg[block]:
for var in phi_blocks.get(succ, []):
add_phi_arg(succ, var, block, cur(var))
# 递归处理支配树子节点
for child in dom_tree.get(block, []):
visit(child)
# 回溯栈
for v in counter:
target = saved.get(v, 0)
while len(stack.get(v, [])) > target:
stack[v].pop()
visit(entry)6.2 完整例子
原始代码: SSA 形式:
Entry: Entry:
x = 1 x_0 = 1
y = 2 y_0 = 2
goto B1 goto B1
B1: B1:
if x < 10 goto B2 x_1 = φ(x_0, x_2)
else B3 y_1 = φ(y_0, y_2)
B2: if x_1 < 10 goto B2 else B3
x = x + 1 B2:
y = y + x x_2 = x_1 + 1
goto B1 y_2 = y_1 + x_2
B3: goto B1
return y B3:
return y_1
七、SSA 上的经典优化
7.1 SCCP:稀疏条件常量传播
SCCP 同时完成常量传播和不可达代码消除,使用三值格:
⊤ (top, 未知)
/ \
c1 c2 ... (具体常量值)
\ /
⊥ (bottom, 非常量)
维护两个工作列表:CFG 工作列表(可达边)和 SSA 工作列表(值变化的定义)。
def sccp(blocks, cfg, entry):
"""Sparse Conditional Constant Propagation"""
TOP, BOT = "__top__", "__bot__"
lat = {}
exe_edges = set()
visited = set()
cfg_wl, ssa_wl = [], []
def get(v):
return lat.get(v, TOP)
def meet(a, b):
if a == TOP: return b
if b == TOP: return a
return a if a == b else BOT
def set_lat(v, val):
old = get(v)
if old == BOT:
return False
nv = meet(old, val)
if nv != old:
lat[v] = nv
return True
return False
def eval_op(op, args):
vals = [get(a) for a in args]
if any(v == BOT for v in vals): return BOT
if any(v == TOP for v in vals): return TOP
a, b = vals[0], vals[1] if len(vals) > 1 else None
if op == "add": return a + b
if op == "sub": return a - b
if op == "mul": return a * b
if op == "lt": return 1 if a < b else 0
return BOT
cfg_wl.append((None, entry))
while cfg_wl or ssa_wl:
while cfg_wl:
src, dst = cfg_wl.pop()
if (src, dst) in exe_edges:
continue
exe_edges.add((src, dst))
if dst not in visited:
visited.add(dst)
for instr in blocks[dst]:
if instr.op == "const":
if set_lat(instr.dst, instr.value):
ssa_wl.append(instr.dst)
elif instr.op == "phi":
val = TOP
for pred, arg in instr.args:
if (pred, dst) in exe_edges:
val = meet(val, get(arg))
if set_lat(instr.dst, val):
ssa_wl.append(instr.dst)
elif instr.dst:
val = eval_op(instr.op, instr.args)
if set_lat(instr.dst, val):
ssa_wl.append(instr.dst)
if instr.op == "br":
succs = cfg[dst]
if len(succs) == 1:
cfg_wl.append((dst, succs[0]))
else:
c = get(instr.args[0])
if c == BOT:
for s in succs:
cfg_wl.append((dst, s))
elif c != TOP:
cfg_wl.append((dst, succs[0 if c else 1]))
while ssa_wl:
var = ssa_wl.pop()
for bname in visited:
for instr in blocks[bname]:
if var in instr.args and instr.dst:
val = eval_op(instr.op, instr.args)
if set_lat(instr.dst, val):
ssa_wl.append(instr.dst)
return lat, visitedSCCP 的强大之处在于它只沿可达的 CFG 边传播。如果条件被证明是常量,只有对应分支会被标记为可达,避免不可达路径上的值”污染”格值。
7.2 GVN:全局值编号
GVN 为每个表达式分配值编号,具有相同值编号的表达式等价:
def gvn(blocks, dom_tree, entry):
"""基于支配树的 GVN(hash-consing)"""
vn_table = {} # (op, vn1, vn2) -> value number
var_vn = {} # ssa_var -> value number
next_vn = [0]
def get_vn(var):
return var_vn.get(var, var)
def lookup_or_add(expr):
if expr in vn_table:
return vn_table[expr]
vn = next_vn[0]
next_vn[0] += 1
vn_table[expr] = vn
return vn
def process(block):
for instr in blocks[block]:
if instr.dst:
expr = (instr.op,) + tuple(get_vn(a) for a in instr.args)
var_vn[instr.dst] = lookup_or_add(expr)
for child in dom_tree.get(block, []):
process(child)
process(entry)
return var_vn由于 SSA 中每个变量只有一个定义,GVN 单遍前序遍历支配树即可——无需迭代到不动点。
7.3 DCE:死代码消除
def aggressive_dce(blocks):
"""积极死代码消除:从有副作用的指令反向标记"""
live = set()
worklist = []
# 有副作用的指令始终活
for bname, instrs in blocks.items():
for instr in instrs:
if instr.op in ("store", "call", "ret", "br"):
live.add(instr)
worklist.append(instr)
# 反向传播
while worklist:
instr = worklist.pop()
for operand in instr.args:
defn = find_def(operand)
if defn and defn not in live:
live.add(defn)
worklist.append(defn)
# 删除非活指令
for bname in blocks:
blocks[bname] = [i for i in blocks[bname] if i in live or not i.dst]7.4 CSE:公共子表达式消除
def cse(blocks, dom_tree, entry):
"""基于支配树的 CSE"""
def process(block, available):
local = dict(available)
for instr in blocks[block]:
if instr.dst and instr.op not in ("call", "store", "load"):
key = (instr.op,) + tuple(instr.args)
if key in local:
replace_all_uses(instr.dst, local[key])
else:
local[key] = instr.dst
for child in dom_tree.get(block, []):
process(child, local)
process(entry, {})支配树的性质保证:祖先节点可用的表达式在后代一定可用。
八、SSA 解构:回到普通形式
8.1 朴素方法与问题
最朴素的方法是在每个前驱块末尾插入拷贝指令:
SSA: 解构后:
B1: x_1=... B1: x_1=... ; x_3=x_1
B2: x_2=... B2: x_2=... ; x_3=x_2
B3: x_3=φ(x_1,x_2) B3: // 无 φ
但这在两种情况下会产生错误。
8.2 Lost-Copy 问题
B1:
x_1 = φ(x_0, x_2)
x_2 = x_1 + 1
if x_2 < 10 goto B1 else B2
如果在回边处插入 x_1 = x_2,而
x_2 的计算依赖于 x_1,拷贝会覆盖
x_1 导致错误。
8.3 Swap 问题
B:
a_2 = φ(a_0, b_1)
b_2 = φ(b_0, a_1)
从回边来的赋值需要同时完成 a_2 = b_1 和
b_2 = a_1,本质上是并行拷贝。
8.4 并行拷贝的线性化
def linearize_parallel_copy(copies):
"""将并行拷贝 [(dst, src), ...] 线性化为顺序拷贝"""
result = []
pending = [(d, s) for d, s in copies if d != s]
while pending:
progress = False
next_round = []
sources = {s for _, s in pending}
for dst, src in pending:
if dst not in sources:
result.append((dst, src))
progress = True
else:
next_round.append((dst, src))
pending = next_round
if not progress and pending:
# 循环依赖——引入临时变量打破
dst, src = pending[0]
temp = f"__tmp_{dst}"
result.append((temp, src))
pending = [(d, temp if s == src else s)
for d, s in pending]
return result8.5 正确的解构算法
Sreedhar 等人(1999)的方案:
- 分裂关键边(critical edge splitting);
- 将 φ 函数转化为并行拷贝;
- 线性化并行拷贝。
def deconstruct_ssa(blocks, phis, cfg):
"""SSA 解构:消除 φ 函数"""
for block, phi_list in phis.items():
if not phi_list:
continue
pred_copies = {}
for phi_var, _, args in phi_list:
for pred, src in args.items():
pred_copies.setdefault(pred, []).append((phi_var, src))
for pred, copies in pred_copies.items():
seq = linearize_parallel_copy(copies)
insert_copies_at_end(pred, seq)
phis[block] = []九、LLVM IR:工业级 SSA
9.1 LLVM IR 示例
define i32 @sum(i32 %n) {
entry:
br label %loop
loop:
%i = phi i32 [0, %entry], [%i.next, %loop]
%s = phi i32 [0, %entry], [%s.next, %loop]
%s.next = add i32 %s, %i
%i.next = add i32 %i, 1
%cond = icmp slt i32 %i.next, %n
br i1 %cond, label %loop, label %exit
exit:
ret i32 %s.next
}每个值只定义一次,phi
指令明确写出值的来源,类型信息显式。
9.2 mem2reg:从 alloca 到 SSA
Clang 先生成基于 alloca/load/store 的非 SSA 代码,然后
mem2reg pass 用 Cytron 算法提升为 SSA:
; 提升前 ; 提升后
%ptr = alloca i32 ; (deleted)
store i32 %x, i32* %ptr ; (deleted)
store i32 42, i32* %ptr ; (deleted)
%val = load i32, i32* %ptr ret i32 42
ret i32 %val9.3 优化管线
mem2reg → instcombine → simplifycfg → gvn → sccp →
dce → loop-simplify → indvars → licm → loop-unroll → ...
可以用 opt 工具逐步观察:
clang -emit-llvm -S -O0 example.c -o example.ll
opt -mem2reg -S example.ll -o example_ssa.ll
opt -sccp -S example_ssa.ll -o example_sccp.ll
opt -gvn -S example_ssa.ll -o example_gvn.ll十、完整 Python 实现
"""SSA Construction + SCCP:完整可运行实现"""
from collections import defaultdict, deque
class Instr:
def __init__(self, op, dst=None, args=None, value=None):
self.op = op
self.dst = dst
self.args = args or []
self.value = value
self._orig = None # φ 的原始变量名
def __repr__(self):
if self.op == "const":
return f"{self.dst} = {self.value}"
if self.op == "phi":
pairs = ", ".join(f"[{v}, {b}]" for b, v in self.args)
return f"{self.dst} = phi({pairs})"
if self.op == "br":
return f"br {', '.join(str(a) for a in self.args)}"
if self.op == "ret":
return f"ret {self.args[0]}"
if self.op in ("add", "sub", "mul", "lt"):
return f"{self.dst} = {self.op} {self.args[0]}, {self.args[1]}"
return f"{self.op} {self.dst} {self.args}"
class BB:
def __init__(self, name):
self.name = name
self.instrs = []
self.succs = []
self.preds = []
class CFG:
def __init__(self):
self.blocks = {}
self.entry = None
def add(self, bb):
self.blocks[bb.name] = bb
if not self.entry:
self.entry = bb.name
def edge(self, a, b):
self.blocks[a].succs.append(b)
self.blocks[b].preds.append(a)
# ---------- 支配树(Cooper-Harvey-Kennedy 迭代算法) ----------
def compute_doms(cfg):
names = list(cfg.blocks.keys())
entry = cfg.entry
doms = {b: set(names) for b in names}
doms[entry] = {entry}
changed = True
while changed:
changed = False
for b in names:
if b == entry or not cfg.blocks[b].preds:
continue
new = set.intersection(*(doms[p] for p in cfg.blocks[b].preds))
new.add(b)
if new != doms[b]:
doms[b] = new
changed = True
return doms
def compute_idom(cfg, doms):
idom = {}
for b in cfg.blocks:
if b == cfg.entry:
continue
strict = doms[b] - {b}
for d in strict:
if all(d2 == d or d not in doms[d2] or d2 not in strict
for d2 in strict):
idom[b] = d
break
return idom
def dom_tree(idom):
ch = defaultdict(list)
for b, d in idom.items():
ch[d].append(b)
return dict(ch)
# ---------- 支配边界 ----------
def compute_df(cfg, idom):
df = {b: set() for b in cfg.blocks}
for y in cfg.blocks:
preds = cfg.blocks[y].preds
if len(preds) < 2:
continue
for x in preds:
r = x
while r and r != idom.get(y):
df[r].add(y)
r = idom.get(r)
return df
# ---------- φ 放置 ----------
def collect_defs(cfg):
defs = defaultdict(set)
for bn, bb in cfg.blocks.items():
for ins in bb.instrs:
if ins.dst:
defs[ins.dst].add(bn)
return defs
def place_phi(df, defs):
phi = defaultdict(set) # block -> {var, ...}
for v, dblks in defs.items():
wl = deque(dblks)
vis = set(dblks)
while wl:
b = wl.popleft()
for y in df.get(b, set()):
if v not in phi[y]:
phi[y].add(v)
if y not in vis:
vis.add(y)
wl.append(y)
return phi
# ---------- 重命名 ----------
def rename(cfg, dtree, phi_map, entry):
cnt = defaultdict(int)
stk = defaultdict(list)
def nn(v):
i = cnt[v]; cnt[v] += 1
name = f"{v}.{i}"; stk[v].append(name); return name
def cur(v):
return stk[v][-1] if stk[v] else f"{v}.undef"
def visit(bn):
bb = cfg.blocks[bn]
saved = {v: len(stk[v]) for v in cnt}
# 插入 φ 指令
new_instrs = []
for v in sorted(phi_map.get(bn, set())):
ins = Instr("phi", dst=nn(v))
ins._orig = v
new_instrs.append(ins)
# 重命名已有指令
for ins in bb.instrs:
ins.args = [
cur(a) if isinstance(a, str) and a.startswith("%") else a
for a in ins.args
]
if ins.dst:
ins.dst = nn(ins.dst)
new_instrs.append(ins)
bb.instrs = new_instrs
# 填充后继 φ 参数
for s in bb.succs:
for ins in cfg.blocks[s].instrs:
if ins.op == "phi":
ins.args.append((bn, cur(ins._orig)))
for ch in dtree.get(bn, []):
visit(ch)
for v in cnt:
while len(stk[v]) > saved.get(v, 0):
stk[v].pop()
visit(entry)
# ---------- 测试 ----------
def build_example():
"""
Entry: x=1, y=2 → B1
B1: if x<10 → B2 else B3
B2: x=x+1, y=y+x → B1
B3: return y
"""
cfg = CFG()
e = BB("entry")
e.instrs = [Instr("const", "%x", value=1),
Instr("const", "%y", value=2),
Instr("br", args=["B1"])]
cfg.add(e)
b1 = BB("B1")
b1.instrs = [Instr("lt", "%cond", ["%x", 10]),
Instr("br", args=["%cond", "B2", "B3"])]
cfg.add(b1)
b2 = BB("B2")
b2.instrs = [Instr("add", "%x", ["%x", 1]),
Instr("add", "%y", ["%y", "%x"]),
Instr("br", args=["B1"])]
cfg.add(b2)
b3 = BB("B3")
b3.instrs = [Instr("ret", args=["%y"])]
cfg.add(b3)
cfg.edge("entry","B1"); cfg.edge("B1","B2")
cfg.edge("B1","B3"); cfg.edge("B2","B1")
return cfg
def main():
print("=" * 55)
print("SSA Construction Demo")
print("=" * 55)
cfg = build_example()
doms = compute_doms(cfg)
idom = compute_idom(cfg, doms)
dt = dom_tree(idom)
df = compute_df(cfg, idom)
defs = collect_defs(cfg)
phi_map = place_phi(df, defs)
print("\nidom:", {b: idom[b] for b in sorted(idom)})
print("DF: ", {b: sorted(df[b]) for b in sorted(df) if df[b]})
print("phi: ", {b: sorted(phi_map[b]) for b in sorted(phi_map) if phi_map[b]})
rename(cfg, dt, phi_map, cfg.entry)
print("\n--- SSA IR ---")
for bn in ["entry", "B1", "B2", "B3"]:
bb = cfg.blocks[bn]
print(f"{bn}: (preds={bb.preds})")
for ins in bb.instrs:
print(f" {ins}")
print("\nSSA verification:", end=" ")
seen = set()
ok = True
for bb in cfg.blocks.values():
for ins in bb.instrs:
if ins.dst:
if ins.dst in seen:
print(f"FAIL: {ins.dst} defined twice"); ok = False
seen.add(ins.dst)
if ok:
print("PASS (every variable defined exactly once)")
if __name__ == "__main__":
main()运行结果:
=======================================================
SSA Construction Demo
=======================================================
idom: {'B1': 'entry', 'B2': 'B1', 'B3': 'B1'}
DF: {'B2': ['B1']}
phi: {'B1': ['%cond', '%x', '%y']}
--- SSA IR ---
entry: (preds=[])
%x.0 = 1
%y.0 = 2
br B1
B1: (preds=['entry', 'B2'])
%cond.0 = phi([entry, %cond.undef], [B2, %cond.1])
%x.1 = phi([entry, %x.0], [B2, %x.2])
%y.1 = phi([entry, %y.0], [B2, %y.2])
%cond.1 = lt %x.1, 10
br %cond.1, B2, B3
B2: (preds=['B1'])
%x.2 = add %x.1, 1
%y.2 = add %y.1, %x.2
br B1
B3: (preds=['B1'])
ret %y.1
SSA verification: PASS (every variable defined exactly once)
十一、工程实践要点
11.1 常见陷阱表
| 陷阱 | 描述 | 解决方案 |
|---|---|---|
| Critical Edge | 多后继 → 多前驱的边 | 插入空中间块(edge splitting) |
| Lost Copy | 解构时拷贝覆盖后续需要的值 | 使用并行拷贝 + 线性化 |
| Swap Problem | 两个 φ 互相依赖 | 并行拷贝或临时变量 |
| φ 爆炸 | 过多不必要的 φ 函数 | Pruned SSA / Semi-pruned SSA |
| 未初始化变量 | SSA 变量引用 undef | 入口块插入显式 undef 定义 |
| 不可约归 CFG | goto 导致的不可约控制流 | 节点分裂(node splitting) |
| 内存 SSA | 指针别名无法直接 SSA 化 | Memory SSA / Array SSA |
11.2 SSA 变体
| 变体 | 特点 | 使用场景 |
|---|---|---|
| Minimal SSA | 最少 φ(Cytron) | 标准实现 |
| Pruned SSA | 只放活跃变量的 φ | 减少 φ 数量 |
| Semi-pruned SSA | 排除纯局部变量 | 折中方案 |
| Conventional SSA | φ 参数和结果不干扰 | 简化寄存器分配 |
| SSI | 合并点和分裂点都插入 | 范围分析 |
| Gated SSA | γ/μ/η 替代 φ | 形式化验证 |
| Sea of Nodes | 节点即值,无显式调度 | HotSpot C2、V8 TurboFan |
11.3 性能参考
| 操作 | 复杂度 | 备注 |
|---|---|---|
| 支配树(Lengauer-Tarjan) | O(E·α(V)) | 约占总编译 1-2% |
| 支配树(Cooper-Harvey-Kennedy) | O(V^2) 最坏 | 实际接近线性 |
| 支配边界 | O(E) | 极快 |
| φ 放置 | O( | Defs |
| 变量重命名 | O(指令数) | 单遍 |
| SCCP | O(SSA 边数) | 近线性 |
| GVN | O(指令数) | 单遍 |
十二、个人思考
SSA 的设计哲学
SSA 的核心洞察:通过增加变量数量来简化数据流关系。每个变量只有一个定义时,变量名本身就编码了定义点信息,省去了昂贵的数据流分析。这种”用空间换信息”的思想在计算机科学中反复出现——数据库范式化、函数式的不可变性、Git 的快照模型。
从学术到工程的鸿沟
教科书上的 SSA 是干净的,工业级编译器中充满工程细节:不可约控制流的处理、MemorySSA 处理 load/store 依赖、调试信息在 SSA 变换后的保持、O(n·α(n)) 与 O(n^2) 在常见规模下实际运行时间的比较。理论上的渐近优势不总是实际优势——常数因子有时更重要。
SSA 在编译器之外
SSA 思想已扩展到编译器之外:静态分析工具(Infer、CodeQL)在 SSA 上做路径敏感分析;反编译器(Ghidra、Hex-Rays)从机器码重建 SSA;JIT 编译器(V8、SpiderMonkey)运行时构建 SSA 并优化。
学习路径
- 手动在纸上完成一个小例子的完整 SSA 构造;
- 实现本文 Python 代码并在不同 CFG 上测试;
- 阅读 LLVM 的
PromoteMemoryToRegister.cpp——工业级 Cytron 实现; - 用
opt逐步跑 pass,观察每步变化。
SSA 是编译器优化的基石。当你下次看到 LLVM IR 中的
phi
指令时,你将不再陌生——那只是来自不同路径的值的汇合点。
参考文献
Cytron, R. et al. (1991). “Efficiently Computing Static Single Assignment Form and the Control Dependence Graph.” ACM TOPLAS, 13(4):451-490.
Lengauer, T. and Tarjan, R.E. (1979). “A Fast Algorithm for Finding Dominators in a Flowgraph.” ACM TOPLAS, 1(1):121-141.
Cooper, K.D., Harvey, T.J., and Kennedy, K. (2001). “A Simple, Fast Dominance Algorithm.” Software Practice and Experience, 4:1-10.
Wegman, M.N. and Zadeck, F.K. (1991). “Constant Propagation with Conditional Branches.” ACM TOPLAS, 13(2):181-210.
Briggs, P. et al. (1998). “Practical Improvements to the Construction and Destruction of Static Single Assignment Form.” SPE, 28(8):859-881.
Sreedhar, V.C. et al. (1999). “Identifying Loops Using DJ Graphs.” ACM TOPLAS, 21(3):538-562.
Appel, A.W. (1998). “SSA is Functional Programming.” ACM SIGPLAN Notices, 33(4):17-20.
LLVM Language Reference Manual. https://llvm.org/docs/LangRef.html
算法系列导航:上一篇:寄存器分配 | 下一篇:垃圾回收算法全景
相关阅读:DFA 最小化 | LR 与 LALR 解析