代码
rust
/// `rewrite_liveranges` 函数是活跃范围重写(Live Range Rewriting)的核心实现
/// 该函数的主要作用是将SSA形式的控制流图转换为传统形式的控制流图
/// 同时合并Phi函数相关的寄存器,为后续的寄存器分配做准备
/// 函数的主要功能
/// 1. 分析Phi函数关系:使用并查集(Union-Find)分析Phi函数中寄存器的等价关系
/// 2. 合并等价寄存器:将Phi函数中相互等价的寄存器合并为同一个寄存器
/// 3. 消除Phi函数:将SSA形式的Phi函数转换为传统的非SSA形式
/// 4. 重写寄存器引用:将所有指令中的寄存器引用更新为合并后的寄存器
fn rewrite_liveranges(mut ssa: CFG<SSAOperator>) -> CFG<Operator> {
/// 步骤1:初始化并查集,用于分析Phi函数中寄存器的等价关系
/// 并查集可以高效地合并和查询集合关系
let mut union_find = util::UnionFind::new();
/// 步骤2:预分配新基本块向量,容量与原SSA CFG相同
let mut new_blocks = Vec::with_capacity(ssa.len());
/// 步骤3:分析所有基本块中的Phi函数,构建寄存器等价关系
for block in ssa.get_blocks() {
/// 获取基本块中指令的迭代器
let mut ops = block.body.iter();
/// 遍历基本块中的所有Phi函数(Phi函数在基本块开头)
while let Some(SSAOperator::Phi(rec, operands)) = ops.next() {
/// 为Phi函数的结果寄存器创建新的集合
union_find.new_set(*rec);
/// 遍历Phi函数的所有操作数寄存器
for &op in operands {
/// 为每个操作数寄存器创建新的集合
union_find.new_set(op);
/// 将结果寄存器与操作数寄存器合并到同一个集合
/// 表示它们是等价的(通过Phi函数关联)
union_find.union(rec, &op);
}
}
}
/// 步骤4:处理所有基本块,重写寄存器引用
/// 使用 std::mem::take 获取所有权,避免额外的复制
for mut block in std::mem::take(&mut ssa.blocks) {
/// 获取基本块中的指令体
let mut old = std::mem::take(&mut block.body);
/// 步骤5:重写每条指令中的寄存器引用
for op in old.iter_mut() {
/// 定义宏:重写寄存器引用
/// 如果寄存器在并查集中有等价寄存器(leader),则替换为leader
macro_rules! rewrite {
($($x:expr),+) => {
{
$(if let Some(leader) = union_find.find($x) {
*$x = *leader;
})+
}
};
}
/// 根据指令类型进行不同的重写处理
match op {
/// 处理普通IR指令
SSAOperator::IROp(op_) => match op_ {
/// 二元运算指令:重写所有寄存器参数
crate::ir::Operator::Add(x, y, z) => rewrite!(x, y, z),
crate::ir::Operator::Sub(x, y, z) => rewrite!(x, y, z),
crate::ir::Operator::Mult(x, y, z) => rewrite!(x, y, z),
crate::ir::Operator::Div(x, y, z) => rewrite!(x, y, z),
crate::ir::Operator::And(x, y, z) => rewrite!(x, y, z),
crate::ir::Operator::Or(x, y, z) => rewrite!(x, y, z),
/// 复制指令:重写源和目标寄存器
crate::ir::Operator::Mv(x, y) => rewrite!(x, y),
/// 其他二元运算指令
crate::ir::Operator::Xor(x, y, z) => rewrite!(x, y, z),
/// 内存访问指令
crate::ir::Operator::Load(x, y, z) => rewrite!(x, y, z),
crate::ir::Operator::Store(x, y, z) => rewrite!(x, y, z),
/// 加载地址指令
crate::ir::Operator::La(x, _) => rewrite!(x),
/// 分支指令:重写比较寄存器
crate::ir::Operator::Bgt(x, y, _, _) => rewrite!(x, y),
crate::ir::Operator::Bl(x, y, _, _) => rewrite!(x, y),
crate::ir::Operator::Beq(x, y, _, _) => rewrite!(x, y),
/// 加载立即数指令
crate::ir::Operator::Li(x, _) => rewrite!(x),
/// 比较指令
crate::ir::Operator::Slt(x, y, z) => rewrite!(x, y, z),
/// 函数调用指令:重写结果寄存器和所有参数寄存器
crate::ir::Operator::Call(x, _, z) => {
rewrite!(x);
for op in z {
rewrite!(op);
}
}
/// 返回指令
crate::ir::Operator::Return(x) => rewrite!(x),
/// 栈内存访问指令
crate::ir::Operator::StoreLocal(x, _) => rewrite!(x),
crate::ir::Operator::LoadLocal(x, _) => rewrite!(x),
/// 获取参数指令
crate::ir::Operator::GetParameter(x, _) => rewrite!(x),
/// 其他指令不需要重写
_ => {}
},
/// Phi函数不需要处理,因为后面会被过滤掉
SSAOperator::Phi(..) => {}
}
}
/// 步骤6:过滤掉Phi函数,只保留普通IR指令
let new: Vec<_> = old
.into_iter()
.filter_map(|op| match op {
/// 保留普通IR指令
SSAOperator::IROp(op) => Some(op),
/// 过滤掉Phi函数
SSAOperator::Phi(_, _) => None,
})
.collect();
/// 步骤7:创建新的基本块,包含重写后的指令
new_blocks.push(block.into_other(new));
}
/// 步骤8:将SSA形式的CFG转换为传统形式的CFG
ssa.into_other(new_blocks)
}
概述
rewrite_liveranges 函数是编译器后端中一个关键步骤,它负责将SSA形式的控制流图转换为传统的非SSA形式
同时合并Phi函数引入的等价寄存器,为后续的寄存器分配扫清障碍
函数核心步骤
构建寄存器等价关系: 遍历所有phi 函数,将phi的结果寄存器与其所有操作数寄存器通过并查集合并为同一个等价类
重写所有寄存器引用: 将每条指令中出现的每个寄存器替换为其等价类的代表(即并查集中该集合的根)
消除phi指令: 删除所有phi指令,只保留普通IR指令
示例:SSA形式的控制流图
假设我们有一下SSA中间表示,它包含两个前驱和一个汇合块:
text
BB1:
%1 = add 2, 3 // 计算结果存入 %1
br BB3
BB2:
%2 = sub 5, 1 // 计算结果存入 %2
br BB3
BB3:
%3 = phi [%1, BB1], [%2, BB2] // 合并两个前驱的值
%4 = add %3, 1
ret %4
步骤1:分析phi函数,构建并查集
遍历到 $3 = phi [%1, BB1], [%2, BB2]:
- 为结果寄存器
%3创建新集合 - 为操作数寄存器
%1和%2创建新集合 - 合并
%3与%1,合并%3和%2, 最终%1、%2、%3属于同一个等价类
假设并查集选择%1 作为该集合的代表(根), 则映射关系为:
%1->%1%2->%1%3->%1
步骤2:重写所有指令的寄存器引用
遍历每个基本块中的每条指令,利用宏 rewrite! 将每个操作数寄存器替换为其代表
BB1:
%1 = add 2, 3:结果寄存器%1替换为%1(不变),操作数无寄存器,无需替换。br BB3: 无寄存器
BB2:
%2 = sub 5, 1:结果寄存器%2替换为%1(因为代表是%1),指令变为%1 = sub 5, 1br BB3:无寄存器。
BB3:
%3 = phi [...]:Phi指令本身不会被重写,后续会被过滤。%4 = add %3, 1:操作数%3替换为%1,结果寄存器%4不变(%4未出现在任何Phi中,仍为原寄存器)。指令变为%4 = add %1, 1ret %4:操作数%4不变
步骤3:过滤所有Phi指令
将Phi指令从基本块中移除,只保留普通IR指令。BB3中的Phi指令被删除。
最终输出的传统形式CFG
text
BB1:
%1 = add 2, 3
br BB3
BB2:
%1 = sub 5, 1 // 原 %2 已被重写为 %1
br BB3
BB3:
%4 = add %1, 1
ret %4
结果分析
Phi指令消失:SSA特有的Phi节点被完全消除
寄存器合并:原来 %1、%2、%3 三个不同的虚拟寄存器现在全部统一为 %1。
这体现了Phi函数的语义:在汇合点 %3 的值要么来自 %1 要么来自 %2,因此这三个寄存器本质上是同一个值的不同副本,可以合并
定义冲突:%1 同时在BB1和BB2中被定义。
在非SSA形式中,同一个虚拟寄存器可以在不同基本块中多次定义,只要它们的活跃范围不重叠(这里显然不重叠),这是合法的,后续寄存器分配会为它们分配不同的物理寄存器或栈位置。
为什么这样做?
为寄存器分配铺路:物理寄存器无法直接实现Phi函数,必须通过复制指令或合并来消除Phi。
该函数通过合并等价寄存器,将Phi的语义转化为普通的数据流,使后续的寄存器分配(如线性扫描、图着色)能够直接处理。
减少寄存器压力:合并等价寄存器可以降低总的虚拟寄存器数量,简化分配。
保持程序语义:重写过程基于并查集保证所有等价寄存器都被统一,确保转换后的程序与原始SSA程序行为一致。
边界情况处理
死Phi函数:如果Phi的操作数中有 u32::MAX(表示未定义),代码中会在前面步骤跳过,但此处重写时,由于并查集中没有该寄存器,不会产生影响。
循环依赖:如果Phi形成循环(如 %1 = phi [%2], %2 = phi [%1]),并查集会正确合并它们为一个等价类,重写后所有引用都指向同一个代表,打破循环依赖。
多个Phi共用的寄存器:并查集能自然处理多个Phi共享同一寄存器的情况,将它们合并到同一集合。