概述
寄存器分配的核心函数 allocate,它采用基于图着色的算法将虚拟寄存器映射到有限的物理寄存器(例如 RISC-V 的 32 个通用寄存器)
整个过程分为几个阶段:构建冲突图、图着色求解、溢出处理、合并优化,最终替换所有寄存器引用。
代码示例
rust
/// `allocate` 函数是寄存器分配(Register Allocation)的核心实现
/// 该函数的主要作用是将虚拟寄存器分配到物理寄存器,使用图着色算法解决寄存器分配问题\
/// 主要功能
/// 1. 构建冲突图:分析虚拟寄存器之间的冲突关系
/// 2. 图着色分配:使用Z3求解器进行图着色
/// 3. 溢出处理:当寄存器不足时,将虚拟寄存器溢出到栈上
/// 4. 合并优化:合并可以共享同一物理寄存器的虚拟寄存器
/// 5. 最终分配:将虚拟寄存器替换为物理寄存器
fn allocate(ssa: CFG<SSAOperator>) -> (CFG<Operator>, HashMap<VReg, RV64Reg>) {
// 步骤1:重写活跃范围,将SSA形式的CFG转换为传统形式的CFG
// 同时合并Phi函数相关的寄存器
let mut lr_cfg = rewrite_liveranges(ssa);
// 步骤2:主分配循环,使用标签'build_allocate进行循环控制
let (graph, coloring) = 'build_allocate: loop {
// 步骤2.1:构建冲突图、可合并寄存器集合和溢出权重
let (mut graph, mut coalescable, mut spill_weights) =
build_interference_graph(&lr_cfg);
// 步骤2.2:固定某些寄存器(如参数寄存器、返回值寄存器)
let mut pins = RISCV64::pin_liveranges(&mut lr_cfg);
// 步骤2.3:如果存在固定寄存器,重新构建冲突图
if !pins.is_empty() {
(graph, coalescable, spill_weights) = build_interference_graph(&lr_cfg);
}
// 步骤2.4:将固定寄存器添加到冲突图中
for lr in pins.keys() {
graph.maybe_insert(*lr);
}
// 步骤2.5:内部循环,尝试进行图着色
loop {
match graph.find_coloring(&pins, coalescable) {
// 情况1:图着色失败(寄存器不足)
Err(_) => {
// 调试输出:显示冲突图
println!("conflict! {}", lr_cfg.to_dot());
// 选择要溢出的活跃范围:选择溢出权重最大的活跃范围
let lr_to_spill = graph
.nodes
.iter()
.max_by_key(|node| spill_weights[&node.live_range])
.unwrap()
.live_range;
// 获取当前分配的栈偏移量
let ar = *lr_cfg.get_allocated_ars_mut();
// 执行溢出操作:将活跃范围溢出到栈上
spill_liverange(&mut lr_cfg, lr_to_spill, ar);
// 增加栈偏移量,为下一个溢出预留空间
*lr_cfg.get_allocated_ars_mut() += 1;
// 继续外层循环,重新尝试分配
continue 'build_allocate;
}
// 情况2:图着色成功
Ok((coloring, coalesced)) => {
// 检查是否有需要合并的寄存器
let empty = coalesced.is_empty();
// 处理所有需要合并的寄存器对
for (lr1, lr2) in coalesced {
// 如果lr1是固定寄存器,将固定关系转移到lr2
if let Some(pin) = pins.get(&lr1) {
debug_assert!(
pins.get(&lr2).is_none() || pins.get(&lr2).unwrap() == pin
);
pins.insert(lr2, *pin);
pins.remove(&lr1);
}
// 在所有基本块中合并lr1到lr2
for block in lr_cfg.get_blocks_mut() {
let mut i = 0;
while i < block.body.len() {
// 情况2.1:删除冗余的复制指令 Mv(lr1, lr2)
if let Operator::Mv(left, right) = block.body[i] {
if left == lr1 && right == lr2 {
block.body.remove(i);
continue;
}
}
// 情况2.2:替换指令中的结果寄存器
let op = &mut block.body[i];
if let Some(rec) = op.receiver_mut() {
if *rec == lr1 {
*rec = lr2;
}
}
// 情况2.3:替换指令中的依赖寄存器
for op in op.dependencies_mut() {
if *op == lr1 {
*op = lr2;
}
}
i += 1;
}
}
}
// 如果没有需要合并的寄存器,分配完成
if empty {
break 'build_allocate (graph, coloring);
} else {
// 有合并操作,重新构建冲突图
(graph, coalescable, spill_weights) =
build_interference_graph(&lr_cfg);
}
}
}
}
};
// 步骤3:将虚拟寄存器替换为物理寄存器
for block in lr_cfg.get_blocks_mut() {
for op in block.body.iter_mut() {
// 替换指令的结果寄存器
if let Some(rec) = op.receiver_mut() {
*rec = graph
.index
.get(rec)
.map(|&idx| <RV64Reg as Into<usize>>::into(coloring[idx]))
.unwrap_or(coloring[0].into()) as u32;
// unwrap = no interferences at all
}
// 替换指令的依赖寄存器
for dep in op.dependencies_mut() {
*dep = graph
.index
.get(dep)
.map(|&idx| <RV64Reg as Into<usize>>::into(coloring[idx]))
.unwrap_or(coloring[0].into()) as u32;
}
}
}
// 调试输出:显示分配后的控制流图
#[cfg(feature = "print-cfgs")]
{
println!("After register allocation:\n{}", lr_cfg.to_dot());
}
// 步骤4:返回分配结果
(
lr_cfg,
coloring
.into_iter()
.map(|color| (<RV64Reg as Into<usize>>::into(color) as VReg, color))
.collect(),
)
}
示例场景
假设我们有一个简单的SSA代码片段(已经过 rewrite_liveranges 转换为传统形式)
text
BB0:
v1 = add 1, 2
v2 = add v1, 3
br cond, BB1, BB2
BB1:
v3 = add v2, 4
v4 = add v3, 5
ret v4
BB2:
v5 = add v2, 6
ret v5
- 虚拟寄存器:
v1, v2, v3, v4, v5 - 物理寄存器数量:假设只有 2 个(
R0, R1),这会造成压力。 - 冲突关系(基于活跃范围分析):
v1与v2冲突(v1在v2定义后仍被使用?实际根据活跃范围,v1在add v1,3后被使用,所以与v2冲突)v2与v3、v5冲突(因为v2在分支两边都被使用)v3与v4冲突(v3定义后立即被v4使用)v4与v5无直接冲突(不同分支)- 另外,可能还有其他冲突。
我们需要用 2 个物理寄存器完成分配,显然会有溢出。
代码逐段解析与示例应用
1. 重写活跃范围(rewrite_liveranges)
rust
let mut lr_cfg = rewrite_liveranges(ssa);
将SSA形式的CFG转换为传统形式,消除Phi函数,合并等价寄存器(通过并查集)。
这步输出已经准备好用于寄存器分配的传统CFG。
2. 主分配循环 'build_allocate
无限循环,直到分配成功。
2.1 构建冲突图、可合并集合、溢出权重
rust
let (mut graph, mut coalescable, mut spill_weights) =
build_interference_graph(&lr_cfg);
从当前CFG中提取虚拟寄存器之间的冲突关系,构建冲突图 graph。
找出可合并的寄存器对(例如复制指令 Mv vx, vy),放入 coalescable。
计算每个虚拟寄存器的溢出权重(通常基于其使用频率、循环深度等),用于决定溢出顺序。
2.2 固定寄存器
rust
let mut pins = RISCV64::pin_liveranges(&mut lr_cfg);
某些虚拟寄存器必须使用特定的物理寄存器(如函数参数、返回值、调用保存寄存器)。
这里 pins 是一个映射,例如 v1 → R0。
2.3 如果存在固定寄存器,重新构建冲突图
rust
if !pins.is_empty() {
(graph, coalescable, spill_weights) = build_interference_graph(&lr_cfg);
}
固定寄存器会引入额外的冲突,因此需要重新构建冲突图,确保这些固定约束被正确编码。
2.4 将固定寄存器添加到冲突图中
rust
for lr in pins.keys() {
graph.maybe_insert(*lr);
}
确保这些寄存器作为节点存在于图中。
2.5 内部循环:尝试图着色
rust
loop {
match graph.find_coloring(&pins, coalescable) {
Err(_) => { ... } // 着色失败
Ok((coloring, coalesced)) => { ... } // 成功
}
}
调用 find_coloring(前面介绍过的使用Z3求解器的函数)尝试为冲突图着色,并最大化合并数量。
情况 A:着色失败(Err)
rust
println!("conflict! {}", lr_cfg.to_dot());
let lr_to_spill = graph
.nodes
.iter()
.max_by_key(|node| spill_weights[&node.live_range])
.unwrap()
.live_range;
let ar = *lr_cfg.get_allocated_ars_mut();
spill_liverange(&mut lr_cfg, lr_to_spill, ar);
*lr_cfg.get_allocated_ars_mut() += 1;
continue 'build_allocate;
选择溢出权重最大的虚拟寄存器(最"昂贵"的)。
调用 spill_liverange 将其值溢出到栈上(插入 store 和 load 指令),并记录栈偏移。
增加栈偏移量,为下一次溢出预留空间。
跳转到外层循环重新开始构建冲突图(因为代码已改变)。
情况 B:着色成功(Ok)
rust
let empty = coalesced.is_empty();
for (lr1, lr2) in coalesced {
// 固定寄存器处理:如果 lr1 是固定的,将固定关系转移到 lr2
if let Some(pin) = pins.get(&lr1) {
pins.insert(lr2, *pin);
pins.remove(&lr1);
}
// 遍历所有基本块,合并 lr1 到 lr2
for block in lr_cfg.get_blocks_mut() {
// 删除冗余的复制指令 Mv(lr1, lr2)
// 替换所有指令中的 lr1 为 lr2(包括结果和依赖)
}
}
if empty {
break 'build_allocate (graph, coloring);
} else {
// 有合并操作,重新构建冲突图并继续循环
(graph, coalescable, spill_weights) = build_interference_graph(&lr_cfg);
}
coloring 是每个虚拟寄存器分配到的物理寄存器编号(0...K-1)。
coalesced 是实际被合并的寄存器对列表。
对于每个合并对,将代码中所有出现 lr1 的地方替换为 lr2,并删除冗余的 Mv lr1, lr2 指令。
如果进行了合并,需要重新构建冲突图(因为寄存器合并后图结构会变),然后继续循环(可能再次尝试着色,但此时合并后的图更容易着色)。
如果没有合并(empty),说明分配完成,跳出外层循环。
3. 最终替换虚拟寄存器为物理寄存器
rust
for block in lr_cfg.get_blocks_mut() {
for op in block.body.iter_mut() {
if let Some(rec) = op.receiver_mut() {
*rec = graph.index.get(rec)
.map(|&idx| coloring[idx].into())
.unwrap_or(coloring[0].into()) as u32;
}
for dep in op.dependencies_mut() {
*dep = graph.index.get(dep)
.map(|&idx| coloring[idx].into())
.unwrap_or(coloring[0].into()) as u32;
}
}
}
利用最终的 coloring 映射,将所有虚拟寄存器替换为物理寄存器编号(例如 0 表示 R0)。
注意这里 coloring[idx] 是物理寄存器的抽象表示(例如 RV64Reg 枚举),通过 into() 转为 usize 作为寄存器编号。
4. 返回结果
rust
(
lr_cfg,
coloring
.into_iter()
.map(|color| (color.into(), color))
.collect(),
)
返回重写后的传统CFG(已替换为物理寄存器)以及虚拟寄存器到物理寄存器的映射(用于调试)。
示例执行过程(假设 2 个物理寄存器)
- 初始状态:有 5 个虚拟寄存器,冲突图较密,2 个物理寄存器不足以着色。Z3 求解器会返回 Err。
- 第一次溢出 :选择溢出权重最大的寄存器(假设为
v4)。调用spill_liverange为v4插入store(在定义后)和load(在每个使用前),并分配栈槽。代码变为:
text
BB0:
v1 = add 1,2
v2 = add v1,3
br cond, BB1, BB2
BB1:
v3 = add v2,4
store v4, [sp+0] // 溢出点
v4 = add v3,5 // 实际 v4 现在存于栈上
load v4, [sp+0] // 使用前重载
ret v4
BB2:
v5 = add v2,6
ret v5
注意:此时 v4 的活跃范围被分割,v4 的冲突减少。
- 重新构建冲突图:新图可能变得稀疏一些。假设仍然无法着色(2个寄存器仍不够),继续溢出。
- 第二次溢出:选择下一个溢出权重最大的,例如 v3。再次插入 store/load,并增加栈偏移。
- 最终着色成功 :经过若干次溢出后,剩余虚拟寄存器数量 ≤ 2,且冲突图可 2-着色。Z3 找到一组分配,例如:
v1→ R0v2→ R1v5→ R0- 等等(注意 v3、v4 已被溢出,不再参与分配)
- 合并处理:如果存在可合并的复制指令(例如 Mv v5, v2),会在这一步合并,减少复制指令。
- 最终替换:将代码中所有虚拟寄存器替换为物理寄存器(包括溢出的 v3、v4 在 store/load 中的使用)。
- 输出:得到物理寄存器分配的机器代码。
总结
allocate 函数通过迭代的"构建冲突图 → 尝试图着色 → 失败则溢出 → 重复"流程,解决了在有限物理寄存器下分配虚拟寄存器的问题。
其核心是使用 Z3 求解器精确求解图着色(包括固定约束和合并优化),并在着色失败时采用启发式溢出策略。
整个过程保证了生成的代码能够正确运行,同时尽可能减少溢出开销。