寄存器分配的核心函数 allocate

概述

寄存器分配的核心函数 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),这会造成压力。
  • 冲突关系(基于活跃范围分析):
    • v1v2 冲突(v1v2 定义后仍被使用?实际根据活跃范围,v1add v1,3 后被使用,所以与 v2 冲突)
    • v2v3v5 冲突(因为 v2 在分支两边都被使用)
    • v3v4 冲突(v3 定义后立即被v4使用)
    • v4v5 无直接冲突(不同分支)
    • 另外,可能还有其他冲突。

我们需要用 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 个物理寄存器)

  1. 初始状态:有 5 个虚拟寄存器,冲突图较密,2 个物理寄存器不足以着色。Z3 求解器会返回 Err。
  2. 第一次溢出 :选择溢出权重最大的寄存器(假设为 v4)。调用 spill_liverangev4 插入 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 的冲突减少。

  1. 重新构建冲突图:新图可能变得稀疏一些。假设仍然无法着色(2个寄存器仍不够),继续溢出。
  2. 第二次溢出:选择下一个溢出权重最大的,例如 v3。再次插入 store/load,并增加栈偏移。
  3. 最终着色成功 :经过若干次溢出后,剩余虚拟寄存器数量 ≤ 2,且冲突图可 2-着色。Z3 找到一组分配,例如:
    • v1 → R0
    • v2 → R1
    • v5 → R0
    • 等等(注意 v3、v4 已被溢出,不再参与分配)
  4. 合并处理:如果存在可合并的复制指令(例如 Mv v5, v2),会在这一步合并,减少复制指令。
  5. 最终替换:将代码中所有虚拟寄存器替换为物理寄存器(包括溢出的 v3、v4 在 store/load 中的使用)。
  6. 输出:得到物理寄存器分配的机器代码。

总结

allocate 函数通过迭代的"构建冲突图 → 尝试图着色 → 失败则溢出 → 重复"流程,解决了在有限物理寄存器下分配虚拟寄存器的问题。

其核心是使用 Z3 求解器精确求解图着色(包括固定约束和合并优化),并在着色失败时采用启发式溢出策略。

整个过程保证了生成的代码能够正确运行,同时尽可能减少溢出开销。

相关推荐
苏三说技术12 小时前
Claude Code从失控到起飞,只用了这些技巧
后端
aqi0012 小时前
15天学会AI应用开发(七)有了大模型为什么还要引入RAG
人工智能·python·大模型·ai编程·ai应用
长栎13 小时前
写 for 循环写了十年,你却从没用过迭代器模式最狠的那一面
后端
LiaCode13 小时前
Redis 在生产项目的使用
前端·后端
用户5598224812213 小时前
Docker Compose Down 导致容器数据误删——ext4 日志恢复全记录
后端
LiaCode13 小时前
一天学完 redis 的爽翻版核心知识总结
前端·后端
大刚测试开发实战13 小时前
如何内网穿透访问本地私有化部署的TestHub
前端·后端·github
xiaodaoluanzha13 小时前
迄今為止,最簡單的編程語言 Nolang
前端·后端
Csvn13 小时前
Docker 容器管理入门 — 从镜像到容器编排
后端