寄存器分配的核心函数 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 求解器精确求解图着色(包括固定约束和合并优化),并在着色失败时采用启发式溢出策略。

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

相关推荐
爱吃烤鸡翅的酸菜鱼1 小时前
Spring Cloud Eureka 服务注册与发现实战详解:从原理到高可用集群搭建
java·spring·spring cloud·eureka
天远云服1 小时前
驾培系统车辆核验实战:PHP集成天远二手车估值API实现学员车辆信息自动化管理
大数据·开发语言·自动化·php
2501_945424801 小时前
高性能计算资源调度
开发语言·c++·算法
程序员爱钓鱼1 小时前
GoWeb开发核心库: net/http深度指南
后端·面试·go
程序员Terry1 小时前
Java 代理模式:从生活中的"中介"到代码中的"代理人"
后端·设计模式
野犬寒鸦1 小时前
JVM垃圾回收机制深度解析(G1篇)(垃圾回收过程及专业名词详解)(补充)
java·服务器·开发语言·jvm·后端·面试
白宇横流学长1 小时前
基于SpringBoot实现的信息技术知识赛系统设计与实现【源码+文档】
java·spring boot·后端
ZHOUPUYU1 小时前
PHP异步编程实战ReactPHP到Swoole的现代方案
开发语言·php
yhyyht2 小时前
Maven命令学习记录(一)
后端