本页目录

SSA 形式

现代编译器优化的基石:每个变量恰好被赋值一次,数据流分析从迭代不动点退化为稀疏图上的单遍扫描。支配树、支配边界和 φ 函数是让这个约束在所有控制流图上成立的三件工具。

概述

常规 IR(三地址码)里,同一个变量可能被多次赋值——数据流分析时,对某个变量 x 的引用,要回溯找到"是哪次赋值产生的"。这在大型函数里不仅是性能问题,更是正确性问题(多个赋值点通过不同路径到达同一使用点)。

SSA(Static Single Assignment) 的核心约束极其简洁:⁠每个变量在程序正文中恰被定义一次⁠。这看似苛刻,但引入 φ 函数(phi function) 后,任何可规约控制流都能转换成 SSA。一旦进入 SSA,数据流分析的很多问题从"迭代求解"变成"单遍扫描"——因为每个值只有一个定义点,use-def 链是确定的,O(n) 而不是 O(n²)。

为什么 SSA 让分析变简单:一个对比

常规 IR:

x = 1           ← def1
if (cond)
    x = 2       ← def2 (覆盖 def1)
y = x + 1       ← 这里的 x 来自 def1 还是 def2? 取决于 cond

要知道 y = x + 1 用了哪次赋值,必须做 reaching definition 分析⁠(迭代数据流,最坏 O(n²))。

SSA 等价形式:

x1 = 1          ← 唯一 def
if (cond)
    x2 = 2      ← 唯一 def
x3 = φ(x1, x2)  ← 根据控制流选择 x1 或 x2
y = x3 + 1      ← 确定用 x3, x3 的定义点就是 φ——不依赖迭代分析

每一个使用点直接指向唯一定义点——稀疏表示(sparse representation),数据流信息不再需要在整个 CFG 上传播,只需在 def-use 图上一遍扫描。这就是 SSA 成为现代优化基础设施的原因。

构造 SSA 的三个关键概念

支配树(dominator tree)

节点 A 支配⁠(dominate)节点 B,当且仅当从入口到 B 的每一条路径都经过 A。支配关系构成一棵树(根是入口块):

支配树:从入口到各基本块的支配关系 entry block1 block2 block3 block4 entry 支配所有块;block1 支配 block2、block3、block4。 block2 与 block3 互相不支配——从 entry 到其一不必经过另一个,二者在 block4 汇合。
  • entry 支配所有块
  • block1 支配 block2, block3, block4
  • block2block3 互相不支配(从 entry 到 block2 不一定经过 block3)

支配树从 CFG 计算:用 Cooper 的工程化算法(O(n·|V|),小规模 CFG 上表现良好)或 Lengauer-Tarjan(O(n log n),大规模 CFG)。dominator 是 SSA 构造的基础——每个变量的定义点在支配树中"覆盖"它后续的可达使用点。

支配边界(dominance frontier)

但变量不会只在"被支配"的点里使用。当控制流汇合时(如 if-else 的结束处),两个不同定义会"碰面"。⁠支配边界精确定义这些汇合点:

DF(X) = {Y | X 支配 Y 的某个前驱, 但 X 不严格支配 Y}

直译:Y 是第一个"不再被 X 支配"的节点——Y 有一个来自 X 支配区域的边,但 Y 本身在 X 的支配区之外。这些 Y 就是需要插 φ 函数的位置。

φ 函数:控制流汇合处的选择器

φ 函数不是真正的指令——它是一个表示法:"我根据实际进入的控制流边,选择对应来源的值"。

x3 = φ(block2→x1, block3→x2)
    ↑ 如果从 block2 进入 → x3 = x1; 如果从 block3 进入 → x3 = x2

在代码生成阶段,φ 函数被消解为寄存器/内存上的实际 move——共享同一物理位置,或从多个来源寄存器合并(解构 SSA)。

Cytron 算法:高效构造 SSA

Cytron(1991)的算法至今是标准——LLVM 和 GCC 都用它。两步:

Step 1:确定 φ 的插入位置

使用支配边界:

对所有定义过变量 v 的块 D:
  对所有 D 的支配边界中的块 F:    ← F 是汇合点
    在 F 入口插 φ(v)
    然后把 F 也当成"定义了 v"的块 (因为 φ 本身也是定义)
    递归到 F 的支配边界

实际上对所有变量同时做——一次遍历支配边界,为每个变量记住需要 φ 的块。

Step 2:重命名变量

在支配树上做前序遍历,维护当前每个变量名对应的版本号栈⁠:

rename(block):
    for each φ in block:
        给 φ 的结果一个新的变量版本 (如 x3)
        把 x → x3 压入 x 的版本栈
    for each 指令 in block:
        对每条指令的每个使用:
            查变量版本栈: 用当前栈顶版本替换
        对每条指令的定义:
            给新版本号 (如 x2)
            把该变量 → 新版本号 压入栈
    for each block 的后继:
        填后继的 φ 参数: 用当前各变量的栈顶版本
    for each block 的支配树子节点:
        rename(child)          ← 递归
    弹出当前 block 压入的所有版本号 ← 出作用域

这个重命名过程在支配树上一次 O(N) 遍历完成所有变量的版本编号——比"每个变量的定义点在 CFG 上做 reaching definition"快一个数量级。

解构 SSA:从 φ 回到可执行代码

SSA 的 φ 函数不是真实指令,代码生成前必须解构(destruction):

解构前:                         解构后 (copy 方式):
  L1: x1 = 1                     L1: x = 1
      goto L3                        goto L3
  L2: x2 = 2                     L2: x = 2
      goto L3                        goto L3
  L3: x3 = φ(x1, x2)             L3:           ← x 已经是正确的值了
      use(x3)                        use(x)

思路:如果 φ 的各个来源可以共享同一寄存器,就不需要额外指令——只需在控制流的前驱块末尾把值放进同一个寄存器。⁠关键是一个 φ 的多个来源不能同时活跃⁠(它们来自互斥的路径),所以共享同一物理位置是安全的。

复杂情况:多个 φ 互相引用,或者 φ 和普通指令有依赖循环(如 x = φ(y, z); y = x + 1,在循环里)——这是"lost copy"和"swap"问题,需要插入临时变量打破循环。Cooper 等人(1998)的经典 out-of-SSA 算法。更高效的实现参考后续研究如 Pereira & Palsberg(2004)。

SSA 在 LLVM 中的地位

LLVM IR 是全 SSA 的:每个虚拟寄存器(%1, %2, ...)只被赋值一次。alloca/load/store 绕过了 SSA(通过内存别名),LLVM 的 mem2reg pass 把能提升的 alloca-store-load 提升为 SSA 虚拟寄存器——这是进入优化 pipeline 的第一关。

一旦进入 SSA,LLVM 的大多数 pass(inlining, GVN, LICM, DCE, loop unrolling)全部基于 SSA 做稀疏分析——这是 LLVM 优化速度快且精准的根本原因。

权衡与失败模式

  • φ 插入过多⁠:支配边界保守计算可能在某些不可达路径上插多余的 φ → SSA 解构后会被死代码消除处理,不影响正确性,但中间占用更多虚拟寄存器。
  • 不可规约控制流(irreducible CFG):goto 或 tail call 导致的 CFG 可能没有支配树意义上的"单入口循环头"——标准算法需要先做 CFG 规约化(node splitting)。
  • 解构 SSA 引入额外 copy:φ 解构可能插入大量 copy 指令 → 后续寄存器分配时,coalescing 把能合并的 copy 消掉(SSA 解构 + 寄存器分配是一对搭档,见 寄存器分配)。

参考

  • Cytron et al. (1991): "Efficiently Computing Static Single Assignment Form and the Control Dependence Graph" — SSA 构造的原始算法
  • Cooper/Torczon: "Engineering a Compiler", Chapters 8–9 (SSA 的完整讲解,含支配树的计算)
  • LLVM: lib/Transforms/Utils/Mem2Reg.cpp, lib/Transforms/Utils/SSAUpdater.cpp — 工业级 SSA 实现

Keywords: SSA, static single assignment, φ-function, phi node, dominator, dominance frontier, Cytron algorithm, renaming, out-of-SSA, SSA destruction, sparse analysis, mem2reg, irreducible CFG, use-def chain, virtual register