Files
nudt-compiler-cpp/doc/Lab4-寄存器分配.md
2026-03-13 16:22:04 +08:00

8.5 KiB
Raw Blame History

Lab4寄存器分配与后端优化

1. 本实验定位

本仓库当前提供了一个“最小可运行”的 IR -> AArch64 汇编示例链路。
Lab4 的目标是在 Lab3 示例基础上,把“固定寄存器 + 栈槽”的最小后端实现推进为“虚拟寄存器 -> 物理寄存器”的真实后端阶段,并在此基础上补充局部后端优化,为完整 SysY 后端打基础。

2. Lab4 要求

需要同学完成:

  1. 熟悉 MIR 中寄存器、操作数、栈槽与机器函数之间的关系,并理解当前 IR -> MIR -> 汇编输出流程中寄存器相关部分的最小实现现状。
  2. 扩展当前 MIR 表达,使指令选择阶段能够产出虚拟寄存器,而不是继续固定使用 w0w8w9
  3. 在现有框架上实现真实寄存器分配,并处理 spill/reload、栈槽管理、callee-saved 保存恢复等后续问题。
  4. 图着色寄存器分配与线性扫描寄存器分配均可作为实现路线,同学可自行选择其中一种完成;后端优化部分也不限定具体实现方式,只要求功能正确、收益明确。
  5. 在寄存器分配结果基础上,补充后端局部优化流程,减少明显冗余机器指令与低效访存。可实现的优化包括但不限于:窥孔优化、冗余 move/copy 消除、局部访存冗余消除,以及简单恒等指令消除(如 add/sub ..., #0)。
  6. test/test_case 提供的全部测试用例上验证正确性,并在保证功能正确的前提下尽量减少冗余 spill/reload、无效拷贝、冗余访存与低效机器指令提升生成代码质量。

3. 相关文件

以下文件与本实验内容相关,建议优先阅读。

  • include/mir/MIR.h
  • src/mir/Lowering.cpp
  • src/mir/RegAlloc.cpp
  • src/mir/FrameLowering.cpp
  • src/mir/passes/Peephole.cpp

4. 当前最小示例实现说明

当前后端中的寄存器分配与后端优化相关实现仍停留在最小示例阶段:

  1. Lowering.cpp 当前直接使用固定物理寄存器 w0w8w9 生成机器指令,而不是先生成虚拟寄存器。
  2. RegAlloc.cpp 当前仅执行最小一致性检查,不实现真实寄存器分配。
  3. 当前 MIR 主要围绕单函数 main、单基本块与最小指令子集工作,尚未形成完整课程版本所需的寄存器分配基础设施。
  4. FrameLowering.cppAsmPrinter.cpp 当前默认前面阶段已经给出可直接落地的固定寄存器结果,并未围绕完整 RA 流程展开。
  5. src/mir/passes/Peephole.cppsrc/mir/passes/PassManager.cpp 当前仅保留了最小注释框架,尚未形成真实可运行的后端优化流程。
  6. 因此,当前代码实际上没有实现完整的寄存器分配与后端优化,这一部分需要同学自行完成。

说明:本阶段不应继续沿用 Lab3 的“所有中间值统一写回栈槽 + 固定寄存器临时搬运”的做法,而应先把指令选择结果改造成带虚拟寄存器的 MIR再进入寄存器分配阶段在寄存器分配与栈帧落地完成后再针对最终机器指令序列做局部后端优化。无论选择哪一种寄存器分配算法都需要先解决几个共同前提为机器指令补充 use/def 信息、能够遍历机器基本块与控制流关系、为虚拟寄存器维护分配状态,并在 spill 后为新引入的访存指令重新参与后续流程。

后端优化部分建议保持“局部、可验证、与当前框架贴合”的范围,不必一开始就追求很重的优化框架。更合适的做法,是先围绕最终机器指令里最常见、最容易验证收益的冗余展开,例如删除明显多余的 move/copy,合并常见的短指令模式,清理无效恒等操作,以及减少相邻、无干扰的重复 load/store。如果寄存器分配或固定模板代码引入了比较机械的搬运和访存,也可以优先从这些最直观的低效模式入手做局部改进。

说明:本实验中的后端优化重点是“局部机器级优化”,并不要求实现全局代码布局优化、复杂指令调度或更高级的机器级分析框架。目标是在保证语义正确的前提下,让最终汇编更紧凑、更直接。

可选的两条常见实现路线如下:

  1. 图着色寄存器分配

    • 整体思路:把“两个虚拟寄存器若在某一程序点同时活跃,则不能分配到同一个物理寄存器”转化为图着色问题。图中的结点表示虚拟寄存器,边表示二者互相干涉;若有 K 个可分配物理寄存器,则目标是对干涉图进行 K 着色。
    • 典型步骤:
      1. 先对 MIR 做活跃性分析,计算各基本块或各指令位置的 live-in/live-out。
      2. 根据活跃信息构建干涉图;若需要优化 move也可以额外记录可合并关系。
      3. 按照可分配寄存器数 K 对图执行 simplify/select必要时结合启发式选择 spill 候选。
      4. 若图可以着色,则回填每个虚拟寄存器对应的物理寄存器;若不能着色,则把选中的虚拟寄存器重写为 spill/reload 形式,并重新进行分析与分配。
      5. 分配完成后,把使用到的 callee-saved 寄存器、额外 spill 栈槽等信息交给 FrameLowering.cppAsmPrinter.cpp 继续处理。
    • 说明:图着色方法可以参考课堂 PPT 中介绍的基本思路来实现。实际工程里这类方法有很多变体,你也可以在这个大方向下结合自己的实现继续调整和优化具体细节;但无论采用哪种变体,都需要补齐活跃性分析、干涉图维护与 spill 重试机制等关键环节。
  2. 线性扫描寄存器分配

    • 整体思路:先把每个虚拟寄存器的活跃范围抽象为一个区间,再按照区间起点顺序扫描程序,动态维护当前正在占用物理寄存器的活跃区间集合;若出现寄存器不够用,再选择某个区间 spill。
    • 典型步骤:
      1. 为机器指令建立稳定顺序,并结合活跃性信息计算每个虚拟寄存器的 live interval。
      2. 按区间起点排序后顺序扫描,维护当前仍然活跃的区间集合 active
      3. 每处理到一个新区间时,先移除已经结束的区间并释放其占用的物理寄存器。
      4. 若存在空闲物理寄存器,则直接分配;若没有空闲寄存器,则比较当前区间与 active 中已有区间的结束位置,选择 spill 当前区间或 spill 一个结束更晚的旧区间。
      5. 对 spill 的虚拟寄存器插入 reload/store 后,需要重新计算受影响区间,再继续后续分配与汇编落地。
    • 说明线性扫描通常更容易先做出一个可运行版本作为寄存器分配的起点也比较常见但如果要把效果做得更好仍然需要认真处理区间切分、调用点约束、callee/caller-saved 寄存器使用策略等问题。

无论采用图着色还是线性扫描,都不应把寄存器分配理解为“把虚拟寄存器简单替换成物理寄存器名字”。真正完整的实现还需要和 spill/reload、栈帧布局、callee-saved 保存恢复以及最终汇编输出联动,否则后端仍然无法支撑完整 SysY 程序。

5. 构建与运行

cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j "$(nproc)"

6. Lab4 验证方式

项目编译后可先用当前示例用例检查后端链路是否仍能运行:

./build/bin/compiler --emit-asm test/test_case/function/simple_add.sy

推荐继续使用统一脚本验证 “源码 -> 汇编 -> 可执行程序” 整体链路。--run 模式下会自动读取同名 .in,并将程序输出与退出码和同名 .out 比对,用于检查单个用例的完整结果:

./scripts/verify_asm.sh test/test_case/function/simple_add.sy test/test_result/function/asm --run

建议在功能回归之外,再观察优化前后汇编输出差异。可按自己的实现方式保留调试日志、优化开关,或直接对比生成的汇编文本,重点关注:

  1. 是否删除了明显冗余的 move/copy 指令。
  2. 是否减少了不必要的 load/store 与重复访存。
  3. 是否消除了无意义的恒等操作。

完成 Lab4 后,最终不应只停留在 simple_add 这一示例用例,而应对 test/test_case 下全部测试用例逐个回归,确保生成代码功能正确;如有需要,也可以自行编写批量测试脚本统一执行。在此基础上,再尽量减少不必要的 spill/reload、无效拷贝、冗余访存与低效机器指令以提升最终性能表现。