静态单赋值(SSA)形式是现代编译器优化的基石。在 SSA 形式中,每个变量只被赋值一次,这使得数据流分析变得简洁高效。本文深入探讨 SSA 的构造算法和基于 SSA 的优化 Pass 设计。
一、支配树与支配边界
SSA 构造依赖于控制流图的支配关系。节点 A 支配节点 B 当且仅当从入口到 B 的所有路径都经过 A。支配边界是插入 Phi 节点的关键位置。
type DominatorTree struct {
idom map[int]int // 直接支配者
children map[int][]int // 支配树子节点
df map[int][]int // 支配边界
}
func (dt *DominatorTree) ComputeDominanceFrontier(cfg *CFG) {
for _, b := range cfg.Blocks {
dt.df[b.ID] = make([]int, 0)
}
for _, b := range cfg.Blocks {
if len(b.Preds) >= 2 {
for _, pred := range b.Preds {
runner := pred
for runner != dt.idom[b.ID] {
dt.df[runner] = append(dt.df[runner], b.ID)
runner = dt.idom[runner]
}
}
}
}
}
二、Phi 节点插入
标准 SSA 构造算法在变量的定义处和支配边界处插入 Phi 节点,然后通过重命名过程消除变量名冲突。
func InsertPhiNodes(cfg *CFG, dt *DominatorTree) {
for _, v := range cfg.Variables {
worklist := make([]int, 0)
visited := make(map[int]bool)
for _, def := range v.DefBlocks {
worklist = append(worklist, def)
}
for len(worklist) > 0 {
blockID := worklist[0]
worklist = worklist[1:]
for _, dfBlock := range dt.df[blockID] {
if !visited[dfBlock] {
cfg.Blocks[dfBlock].AddPhi(v)
visited[dfBlock] = true
worklist = append(worklist, dfBlock)
}
}
}
}
}
三、变量重命名
以支配树先序遍历,为每个变量维护一个版本栈。遇到定义时压入新版本,遇到使用时取栈顶版本,离开作用域时弹出。
func RenameVariables(cfg *CFG, dt *DominatorTree) {
stacks := make(map[string]*Stack)
counters := make(map[string]int)
var rename func(blockID int)
rename = func(blockID int) {
block := cfg.Blocks[blockID]
// 重命名 Phi 节点
for _, phi := range block.Phis {
newName := fmt.Sprintf("%s_%d", phi.Var, counters[phi.Var])
counters[phi.Var]++
stacks[phi.Var].Push(newName)
phi.Rename(newName)
}
// 重命名普通指令
for _, inst := range block.Instructions {
for _, use := range inst.Uses {
inst.ReplaceUse(use, stacks[use].Top())
}
if inst.HasDef() {
newName := fmt.Sprintf("%s_%d", inst.Def, counters[inst.Def])
counters[inst.Def]++
stacks[inst.Def].Push(newName)
inst.RenameDef(newName)
}
}
// 填充后继的 Phi 节点参数
for _, succ := range block.Succs {
for _, phi := range cfg.Blocks[succ].Phis {
phi.AddArgument(blockID, stacks[phi.Var].Top())
}
}
// 递归处理支配树子节点
for _, child := range dt.children[blockID] {
rename(child)
}
// 弹出本块压入的版本
for _, inst := range block.Instructions {
if inst.HasDef() {
stacks[inst.Def].Pop()
}
}
}
rename(cfg.Entry)
}
四、基于 SSA 的优化 Pass
SSA 形式使许多优化变得简单:常量传播只需检查定义是否为常量;死代码消除只需检查变量是否有使用;全局值编号利用 SSA 的哈希等价性。
// 稀疏条件常量传播(SCCP)
func SCCP(cfg *CFG) {
lattice := make(map[string]LatticeValue)
worklist := initWorklist(cfg)
for len(worklist) > 0 {
inst := worklist.Pop()
newVal := evaluate(inst, lattice)
if newVal != lattice[inst.Def] {
lattice[inst.Def] = newVal
for _, use := range inst.Uses {
worklist.Push(use)
}
}
}
// 用常量替换变量
for name, val := range lattice {
if val.IsConstant() {
replaceAllUses(name, val.Constant)
}
}
}
五、SSA 的破坏与恢复
某些优化(如寄存器分配)需要破坏 SSA 形式。出 SSA 的标准方法是将 Phi 节点替换为对应前驱块中的复制指令,然后通过合并优化消除冗余复制。
← 返回文章列表