土法炼钢兴趣小组的算法知识备份

SSA 形式与编译器优化

目录

一、引言:为什么需要 SSA

在编译器后端的世界里,中间表示(IR)的选择直接决定了优化的上限。传统的三地址码虽然直观,但变量可以被多次赋值,这使得数据流分析变得复杂——你必须追踪每一个变量在每一个程序点上可能的值。

Static Single Assignment(SSA)形式提出了一个约束:每个变量只能被赋值恰好一次。这个简单的不变式带来了深远的影响:

  1. def-use chain 天然唯一——每个使用点只对应一个定义点;
  2. 大量经典优化在 SSA 上变得更高效;
  3. 寄存器分配可以利用 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),当且仅当从 Entryn每一条路径都经过 d

基本性质:

严格支配d sdom n 当且仅当 d dom nd ≠ 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
支配树与支配边界

四、Lengauer-Tarjan 算法

4.1 算法概述

计算支配树的朴素方法需要 O(V^2) 甚至 O(V^3) 时间。Lengauer-Tarjan(1979)算法的时间复杂度为 O(E·α(V)),α 是反 Ackermann 函数,实际可视为常数。

核心步骤:

  1. DFS 编号:对 CFG 进行深度优先搜索,分配 DFS 序号;
  2. 半支配者(semidominator):利用 DFS 树路径计算;
  3. 从 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)

算法需要高效回答查询:给定路径上的节点,找到 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] = v

4.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 支配 B3B3 → 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 df

5.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 phi

5.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, visited

SCCP 的强大之处在于它只沿可达的 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_1b_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 result

8.5 正确的解构算法

Sreedhar 等人(1999)的方案:

  1. 分裂关键边(critical edge splitting);
  2. 将 φ 函数转化为并行拷贝;
  3. 线性化并行拷贝。
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 %val

9.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 并优化。

学习路径

  1. 手动在纸上完成一个小例子的完整 SSA 构造;
  2. 实现本文 Python 代码并在不同 CFG 上测试;
  3. 阅读 LLVM 的 PromoteMemoryToRegister.cpp——工业级 Cytron 实现;
  4. opt 逐步跑 pass,观察每步变化。

SSA 是编译器优化的基石。当你下次看到 LLVM IR 中的 phi 指令时,你将不再陌生——那只是来自不同路径的值的汇合点。

参考文献

  1. Cytron, R. et al. (1991). “Efficiently Computing Static Single Assignment Form and the Control Dependence Graph.” ACM TOPLAS, 13(4):451-490.

  2. Lengauer, T. and Tarjan, R.E. (1979). “A Fast Algorithm for Finding Dominators in a Flowgraph.” ACM TOPLAS, 1(1):121-141.

  3. Cooper, K.D., Harvey, T.J., and Kennedy, K. (2001). “A Simple, Fast Dominance Algorithm.” Software Practice and Experience, 4:1-10.

  4. Wegman, M.N. and Zadeck, F.K. (1991). “Constant Propagation with Conditional Branches.” ACM TOPLAS, 13(2):181-210.

  5. Briggs, P. et al. (1998). “Practical Improvements to the Construction and Destruction of Static Single Assignment Form.” SPE, 28(8):859-881.

  6. Sreedhar, V.C. et al. (1999). “Identifying Loops Using DJ Graphs.” ACM TOPLAS, 21(3):538-562.

  7. Appel, A.W. (1998). “SSA is Functional Programming.” ACM SIGPLAN Notices, 33(4):17-20.

  8. LLVM Language Reference Manual. https://llvm.org/docs/LangRef.html


算法系列导航上一篇:寄存器分配 | 下一篇:垃圾回收算法全景

相关阅读DFA 最小化 | LR 与 LALR 解析


By .