|
|
|
|
@ -1,19 +1,19 @@
|
|
|
|
|
# Lab3:指令选择与汇编生成说明
|
|
|
|
|
|
|
|
|
|
## 1. 文档目标
|
|
|
|
|
## 1. 文档范围
|
|
|
|
|
|
|
|
|
|
本文档说明当前仓库中 Lab3 后端的真实实现方案,重点回答四个问题:
|
|
|
|
|
本文档描述当前仓库中 Lab3 后端的真实实现,而不是计划中的设计。内容覆盖以下四部分:
|
|
|
|
|
|
|
|
|
|
1. Lab3 后端的整体流水线是什么。
|
|
|
|
|
2. 当前实现与 `Reference` 目录下三份资料的对应关系是什么。
|
|
|
|
|
3. 指令选择、寄存器分配、栈布局分别是如何落地的。
|
|
|
|
|
4. 当前测试结果收敛到了什么程度。
|
|
|
|
|
- Lab3 后端的整体流水线与模块划分
|
|
|
|
|
- 当前实现与 `Reference` 目录下三份资料的对应关系
|
|
|
|
|
- 近期关键正确性问题的定位与修复
|
|
|
|
|
- 当前测试规范与最新测试结论
|
|
|
|
|
|
|
|
|
|
本文档只描述仓库当前代码,不把“计划中的优化”写成“已经完成的实现”。
|
|
|
|
|
本文档对应的是仓库当前代码状态。
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 2. 参考依据
|
|
|
|
|
## 2. 参考资料与采用方式
|
|
|
|
|
|
|
|
|
|
Lab3 当前实现主要参考以下三份资料:
|
|
|
|
|
|
|
|
|
|
@ -21,52 +21,52 @@ Lab3 当前实现主要参考以下三份资料:
|
|
|
|
|
- `Reference/lecture05-instruction selection-169.pdf`
|
|
|
|
|
- `Reference/lecture11-register allocation-part2-169.pdf`
|
|
|
|
|
|
|
|
|
|
对这三份资料的吸收方式如下:
|
|
|
|
|
这三份资料在项目中的落点分别如下:
|
|
|
|
|
|
|
|
|
|
- 栈布局与函数调用约定参考 `lab03`
|
|
|
|
|
采用 AArch64 / AAPCS64 基本规则,使用 `x29` 作为帧指针,栈帧按 16 字节对齐,区分 caller-saved 与 callee-saved 寄存器。
|
|
|
|
|
- 指令选择参考 `lecture05`
|
|
|
|
|
当前实现采用“宏扩展式 lowering + 局部 peephole / 融合优化”的工程化方案,而不是完整树覆盖或 SelectionDAG 风格的树模式匹配。
|
|
|
|
|
- 寄存器分配参考 `lecture11`
|
|
|
|
|
当前实现采用 George 风格图着色寄存器分配,包含 `build / simplify / coalesce / freeze / spill / select` 这一套核心流程。
|
|
|
|
|
- `lab03`
|
|
|
|
|
主要对应栈布局、函数序言和尾声、AAPCS64 调用约定、栈上传参和 16 字节对齐。
|
|
|
|
|
- `lecture05`
|
|
|
|
|
主要对应 instruction selection 的方法论。当前仓库采用的是“宏扩展式 lowering + 局部模式融合”的工程化方案,而不是完整树覆盖或 SelectionDAG。
|
|
|
|
|
- `lecture11`
|
|
|
|
|
主要对应寄存器分配。当前仓库使用的是 George 风格图着色分配,而不是线性扫描。
|
|
|
|
|
|
|
|
|
|
因此,当前项目不是“逐页复刻讲义代码”,而是在讲义方法论基础上完成了适合本仓库的实现。
|
|
|
|
|
因此,当前实现不是逐页照搬讲义,而是按讲义方法论落到本项目结构中。
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 3. 当前后端总览
|
|
|
|
|
## 3. 后端整体流水线
|
|
|
|
|
|
|
|
|
|
当前 `compiler --emit-asm` 的真实执行流程如下:
|
|
|
|
|
当前 `compiler --emit-asm` 的主流程如下:
|
|
|
|
|
|
|
|
|
|
1. ANTLR 前端解析源程序。
|
|
|
|
|
2. 语义分析构建符号与类型信息。
|
|
|
|
|
3. IR 生成得到 LLVM 风格中间表示。
|
|
|
|
|
4. IR Pass Pipeline 做中端优化。
|
|
|
|
|
1. 前端基于 ANTLR 解析 SysY 源程序。
|
|
|
|
|
2. 语义分析建立类型和符号信息。
|
|
|
|
|
3. IR 生成阶段产出 LLVM 风格中间表示。
|
|
|
|
|
4. IR Pass Pipeline 做中端标量优化。
|
|
|
|
|
5. `LowerToMIR` 将 IR 降到自定义 MIR。
|
|
|
|
|
6. `RunRegAlloc` 对 MIR 虚拟寄存器做图着色分配。
|
|
|
|
|
7. `RunFrameLowering` 计算栈对象偏移与最终栈帧大小。
|
|
|
|
|
7. `RunFrameLowering` 计算栈对象偏移和最终帧大小。
|
|
|
|
|
8. `PrintAsm` 输出 AArch64 汇编。
|
|
|
|
|
|
|
|
|
|
对应入口见 [src/main.cpp](/home/hw/nudt-compiler-cpp/src/main.cpp:1)。
|
|
|
|
|
入口在 [src/main.cpp](../src/main.cpp)。
|
|
|
|
|
|
|
|
|
|
这意味着当前 Lab3 后端已经是项目内自研后端,不依赖外部 LLVM 后端来生成目标汇编。
|
|
|
|
|
这意味着 Lab3 已经不依赖 LLVM 后端生成汇编,而是使用仓库内自研的 MIR 后端。
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 4. 模块划分
|
|
|
|
|
## 4. 核心模块划分
|
|
|
|
|
|
|
|
|
|
### 4.1 MIR 基础设施
|
|
|
|
|
|
|
|
|
|
核心文件:
|
|
|
|
|
|
|
|
|
|
- [include/mir/MIR.h](/home/hw/nudt-compiler-cpp/include/mir/MIR.h:1)
|
|
|
|
|
- [src/mir/MIRInstr.cpp](/home/hw/nudt-compiler-cpp/src/mir/MIRInstr.cpp:1)
|
|
|
|
|
- [src/mir/MIRBasicBlock.cpp](/home/hw/nudt-compiler-cpp/src/mir/MIRBasicBlock.cpp:1)
|
|
|
|
|
- [src/mir/MIRFunction.cpp](/home/hw/nudt-compiler-cpp/src/mir/MIRFunction.cpp:1)
|
|
|
|
|
- [src/mir/MIRContext.cpp](/home/hw/nudt-compiler-cpp/src/mir/MIRContext.cpp:1)
|
|
|
|
|
- [src/mir/Register.cpp](/home/hw/nudt-compiler-cpp/src/mir/Register.cpp:1)
|
|
|
|
|
- [include/mir/MIR.h](../include/mir/MIR.h)
|
|
|
|
|
- [src/mir/MIRInstr.cpp](../src/mir/MIRInstr.cpp)
|
|
|
|
|
- [src/mir/MIRBasicBlock.cpp](../src/mir/MIRBasicBlock.cpp)
|
|
|
|
|
- [src/mir/MIRFunction.cpp](../src/mir/MIRFunction.cpp)
|
|
|
|
|
- [src/mir/MIRContext.cpp](../src/mir/MIRContext.cpp)
|
|
|
|
|
- [src/mir/Register.cpp](../src/mir/Register.cpp)
|
|
|
|
|
|
|
|
|
|
这一层定义了后端使用的核心对象:
|
|
|
|
|
这一层定义了:
|
|
|
|
|
|
|
|
|
|
- `MachineOperand`
|
|
|
|
|
- `AddressExpr`
|
|
|
|
|
@ -77,48 +77,56 @@ Lab3 当前实现主要参考以下三份资料:
|
|
|
|
|
- `StackObject`
|
|
|
|
|
- `Allocation`
|
|
|
|
|
|
|
|
|
|
当前 MIR 能表达:
|
|
|
|
|
当前 MIR 能表达的核心语义包括:
|
|
|
|
|
|
|
|
|
|
- 整数算术与位运算
|
|
|
|
|
- 浮点算术
|
|
|
|
|
- `load/store`
|
|
|
|
|
- `lea`
|
|
|
|
|
- 比较与条件跳转
|
|
|
|
|
- 函数调用与返回
|
|
|
|
|
- 比较、跳转与返回
|
|
|
|
|
- `load/store/lea`
|
|
|
|
|
- 函数调用
|
|
|
|
|
- `memset`
|
|
|
|
|
- 类型转换
|
|
|
|
|
- 整浮转换
|
|
|
|
|
|
|
|
|
|
MIR 的作用不是完全等价于 AArch64 汇编,而是作为“比 IR 更接近机器、但仍保留寄存器和地址表达式抽象”的中间层,便于后续做寄存器分配和栈帧落地。
|
|
|
|
|
|
|
|
|
|
### 4.2 IR 到 MIR 的 lowering
|
|
|
|
|
|
|
|
|
|
核心文件:
|
|
|
|
|
|
|
|
|
|
- [src/mir/Lowering.cpp](/home/hw/nudt-compiler-cpp/src/mir/Lowering.cpp:1)
|
|
|
|
|
- [src/mir/Lowering.cpp](../src/mir/Lowering.cpp)
|
|
|
|
|
|
|
|
|
|
职责包括:
|
|
|
|
|
|
|
|
|
|
- IR 指令到 MIR 指令的逐条翻译
|
|
|
|
|
- `alloca` 到栈对象的转换
|
|
|
|
|
- `load/store/gep` 到地址表达式的转换
|
|
|
|
|
- `phi` 结点结果预分配与并行 copy lowering
|
|
|
|
|
- 直接调用的 MIR 生成
|
|
|
|
|
- `phi` 结点预分配与并行 copy lowering
|
|
|
|
|
- 控制流和分支的 MIR 化
|
|
|
|
|
- 直接调用的 MIR 构造
|
|
|
|
|
|
|
|
|
|
### 4.3 寄存器分配
|
|
|
|
|
|
|
|
|
|
核心文件:
|
|
|
|
|
|
|
|
|
|
- [src/mir/RegAlloc.cpp](/home/hw/nudt-compiler-cpp/src/mir/RegAlloc.cpp:1)
|
|
|
|
|
- [src/mir/RegAlloc.cpp](../src/mir/RegAlloc.cpp)
|
|
|
|
|
|
|
|
|
|
当前实现的是 `GeorgeColoringAllocator`,负责:
|
|
|
|
|
|
|
|
|
|
当前实现的是 `GeorgeColoringAllocator`,不是线性扫描。
|
|
|
|
|
- 活跃性分析
|
|
|
|
|
- 干涉图构建
|
|
|
|
|
- move-related coalescing
|
|
|
|
|
- spill 选择
|
|
|
|
|
- 颜色分配
|
|
|
|
|
|
|
|
|
|
### 4.4 栈帧与对象布局
|
|
|
|
|
|
|
|
|
|
核心文件:
|
|
|
|
|
|
|
|
|
|
- [src/mir/FrameLowering.cpp](/home/hw/nudt-compiler-cpp/src/mir/FrameLowering.cpp:1)
|
|
|
|
|
- [src/mir/FrameLowering.cpp](../src/mir/FrameLowering.cpp)
|
|
|
|
|
|
|
|
|
|
职责包括:
|
|
|
|
|
这一层负责:
|
|
|
|
|
|
|
|
|
|
- 局部栈对象布局
|
|
|
|
|
- 局部对象布局
|
|
|
|
|
- spill 槽布局
|
|
|
|
|
- callee-saved 保存槽布局
|
|
|
|
|
- 栈对象偏移计算
|
|
|
|
|
@ -128,245 +136,392 @@ Lab3 当前实现主要参考以下三份资料:
|
|
|
|
|
|
|
|
|
|
核心文件:
|
|
|
|
|
|
|
|
|
|
- [src/mir/AsmPrinter.cpp](/home/hw/nudt-compiler-cpp/src/mir/AsmPrinter.cpp:1)
|
|
|
|
|
- [src/mir/AsmPrinter.cpp](../src/mir/AsmPrinter.cpp)
|
|
|
|
|
|
|
|
|
|
职责包括:
|
|
|
|
|
这一层负责:
|
|
|
|
|
|
|
|
|
|
- MIR 到 AArch64 汇编文本的最终选择
|
|
|
|
|
- 地址模式发射
|
|
|
|
|
- MIR 到 AArch64 汇编文本的最终映射
|
|
|
|
|
- 地址模式选择
|
|
|
|
|
- 调用约定落地
|
|
|
|
|
- 序言 / 尾声生成
|
|
|
|
|
- 序言和尾声生成
|
|
|
|
|
- 全局变量与常量区输出
|
|
|
|
|
|
|
|
|
|
从实现风格上说,真正的“最终 instruction selection”并不只发生在 `Lowering.cpp`,而是由 `Lowering.cpp` 和 `AsmPrinter.cpp` 共同完成。
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 5. 与 `lab03` 的对应关系
|
|
|
|
|
## 5. IR 到 MIR 的实现方式
|
|
|
|
|
|
|
|
|
|
### 5.1 标量指令 lowering
|
|
|
|
|
|
|
|
|
|
在 [src/mir/Lowering.cpp](../src/mir/Lowering.cpp) 中,以下 IR 会逐条映射成 MIR:
|
|
|
|
|
|
|
|
|
|
- `Add/Sub/Mul/Div/Rem`
|
|
|
|
|
- `FAdd/FSub/FMul/FDiv/FNeg`
|
|
|
|
|
- `ICmp/FCmp`
|
|
|
|
|
- `Zext/IToF/FtoI`
|
|
|
|
|
- `Call/Ret`
|
|
|
|
|
- `Br/CondBr`
|
|
|
|
|
|
|
|
|
|
这种做法对应 `lecture05` 中的 macro-expansion / one-by-one translation。
|
|
|
|
|
|
|
|
|
|
`lab03` 的关键点是“基于宏扩展、自顶向下逐条翻译、正确实现 ARMv8 调用约定与栈帧”。
|
|
|
|
|
### 5.2 地址表达式 lowering
|
|
|
|
|
|
|
|
|
|
当前实现与其对应关系如下:
|
|
|
|
|
内存访问不会立即固定成某一条 AArch64 指令,而是先保存在 `AddressExpr` 中。它可以表达:
|
|
|
|
|
|
|
|
|
|
- 自顶向下逐条翻译:符合
|
|
|
|
|
在 [src/mir/Lowering.cpp](/home/hw/nudt-compiler-cpp/src/mir/Lowering.cpp:1) 中,IR 指令被逐条转换为 MIR。
|
|
|
|
|
- 栈帧按 16 字节对齐:符合
|
|
|
|
|
在 [src/mir/FrameLowering.cpp](/home/hw/nudt-compiler-cpp/src/mir/FrameLowering.cpp:1) 中对最终 `frame_size` 做了 16 字节对齐。
|
|
|
|
|
- 使用 `x29` 作为帧指针:符合
|
|
|
|
|
在 [src/mir/AsmPrinter.cpp](/home/hw/nudt-compiler-cpp/src/mir/AsmPrinter.cpp:889) 附近可以看到标准的 `stp x29, x30`、`mov x29, sp`、`mov sp, x29`、`ldp x29, x30`。
|
|
|
|
|
- 整型参数 / 返回值遵守 AAPCS64:符合
|
|
|
|
|
整型参数按 `x0-x7`,浮点参数按 `v0-v7`,超出部分走栈,见 [src/mir/AsmPrinter.cpp](/home/hw/nudt-compiler-cpp/src/mir/AsmPrinter.cpp:843) 起的参数位置计算逻辑。
|
|
|
|
|
- caller-saved / callee-saved 区分:符合
|
|
|
|
|
当前分配器主要使用 `x19-x28` 与 `v8-v15` 作为可分配寄存器,并在需要时保存恢复。
|
|
|
|
|
- 基址来自栈对象
|
|
|
|
|
- 基址来自全局符号
|
|
|
|
|
- 基址来自寄存器
|
|
|
|
|
- 常量偏移
|
|
|
|
|
- 缩放索引寄存器
|
|
|
|
|
|
|
|
|
|
因此,从“课程实验的基础代码生成要求”来看,当前实现总体符合 `lab03` 的设计方向。
|
|
|
|
|
这样做的好处是:
|
|
|
|
|
|
|
|
|
|
- `getelementptr` 可以先降成统一地址表达式
|
|
|
|
|
- 寄存器分配完成后再决定能否发成直接 indexed addressing
|
|
|
|
|
- `lea + load/store` 是否融合可以推迟到汇编打印阶段
|
|
|
|
|
|
|
|
|
|
### 5.3 `phi` lowering
|
|
|
|
|
|
|
|
|
|
`phi` 不是直接发成 MIR 指令,而是在 lowering 时分两步处理:
|
|
|
|
|
|
|
|
|
|
1. 先为每个 `phi` 结果预分配目标 vreg。
|
|
|
|
|
2. 再按 CFG 边收集 copy,并在前驱边上发射并行 copy。
|
|
|
|
|
|
|
|
|
|
对于条件跳转前驱,如果直接在原块尾部插入 copy 可能破坏 terminator 结构,因此实现里会在需要时插入专用边块。
|
|
|
|
|
|
|
|
|
|
这是 Lab3 正确性最关键的一部分之一,后文会专门说明修复细节。
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 6. 与 `lecture05` 的对应关系
|
|
|
|
|
## 6. 指令选择实现说明
|
|
|
|
|
|
|
|
|
|
`lecture05` 讲的是 instruction selection 的方法论,尤其区分了:
|
|
|
|
|
### 6.1 与 `lecture05` 的关系
|
|
|
|
|
|
|
|
|
|
`lecture05` 讲的是 instruction selection 的三类主要思路:
|
|
|
|
|
|
|
|
|
|
- 宏扩展
|
|
|
|
|
- 树模式匹配
|
|
|
|
|
- 窥孔优化
|
|
|
|
|
|
|
|
|
|
当前实现最接近下面这条路线:
|
|
|
|
|
|
|
|
|
|
- 先做宏扩展式 lowering
|
|
|
|
|
将 IR 指令逐条翻译成 MIR 指令。
|
|
|
|
|
- 再做局部模式融合
|
|
|
|
|
在汇编打印阶段把 MIR 指令组合成更自然的 AArch64 指令序列。
|
|
|
|
|
当前仓库最接近的路线是:
|
|
|
|
|
|
|
|
|
|
这体现在两层:
|
|
|
|
|
- 先做宏扩展 lowering
|
|
|
|
|
- 再在汇编发射阶段做局部模式融合
|
|
|
|
|
|
|
|
|
|
### 6.1 宏扩展 lowering
|
|
|
|
|
因此,当前实现符合 `lecture05` 的思想范围,但不是树覆盖式 instruction selector。
|
|
|
|
|
|
|
|
|
|
在 [src/mir/Lowering.cpp](/home/hw/nudt-compiler-cpp/src/mir/Lowering.cpp:1) 中,以下 IR 指令会直接降成对应 MIR:
|
|
|
|
|
### 6.2 当前实际做了哪些选择和融合
|
|
|
|
|
|
|
|
|
|
- `Add/Sub/Mul/Div/Rem`
|
|
|
|
|
- `FAdd/FSub/FMul/FDiv`
|
|
|
|
|
- `ICmp/FCmp`
|
|
|
|
|
- `Zext/IToF/FtoI`
|
|
|
|
|
- `Load/Store`
|
|
|
|
|
- `GetElementPtr`
|
|
|
|
|
- `Call/Return`
|
|
|
|
|
在 [src/mir/AsmPrinter.cpp](../src/mir/AsmPrinter.cpp) 中,当前已经实现了多类工程化 instruction selection:
|
|
|
|
|
|
|
|
|
|
这种做法符合讲义中“macro-expansion / one-by-one translation”的基本思路。
|
|
|
|
|
- `icmp/fcmp + condbr` 的融合发射
|
|
|
|
|
- `lea + load/store` 的直接访存融合
|
|
|
|
|
- 基址加缩放索引的直接寻址
|
|
|
|
|
- `add/sub` 的立即数特化
|
|
|
|
|
- `rem` 到 `sdiv + msub` 的展开
|
|
|
|
|
- 立即数物化到寄存器
|
|
|
|
|
- spill/load/store 到统一的帧地址访问
|
|
|
|
|
|
|
|
|
|
### 6.2 局部模式融合
|
|
|
|
|
因此,Lab3 当前不是“先生成一份一比一 MIR,再无脑打印汇编”,而是保留了机器相关的组合空间。
|
|
|
|
|
|
|
|
|
|
在 [src/mir/AsmPrinter.cpp](/home/hw/nudt-compiler-cpp/src/mir/AsmPrinter.cpp:699) 之后,当前实现做了若干局部优化:
|
|
|
|
|
### 6.3 当前实现与 LLVM 后端的差异
|
|
|
|
|
|
|
|
|
|
- 比较 + 条件跳转融合
|
|
|
|
|
- `lea + load/store` 融合
|
|
|
|
|
- 直接 indexed addressing 发射
|
|
|
|
|
- `add/sub` 的立即数特化
|
|
|
|
|
- `rem` 选择为 `sdiv + msub`
|
|
|
|
|
虽然当前全量样例已经通过,但代码生成质量和 LLVM 后端仍然不是同一层级。当前实现仍然有这些特征:
|
|
|
|
|
|
|
|
|
|
因此,当前实现属于:
|
|
|
|
|
- 没有完整树模式匹配
|
|
|
|
|
- 没有 SelectionDAG 或 GlobalISel
|
|
|
|
|
- 没有大规模机器级组合优化
|
|
|
|
|
- 可分配寄存器集合偏保守
|
|
|
|
|
|
|
|
|
|
- 符合 `lecture05` 的宏扩展与局部模式优化思想
|
|
|
|
|
- 但不是完整的树模式匹配 / 瓦片平铺实现
|
|
|
|
|
所以更准确的描述是:
|
|
|
|
|
|
|
|
|
|
如果要求“完全按树覆盖方式实现 instruction selection”,则当前实现并不属于那一类;但如果要求“以讲义方法论为参考完成工程化指令选择”,则当前实现是符合的。
|
|
|
|
|
- 当前实现已经满足 Lab3 的正确性与基本性能要求
|
|
|
|
|
- 但不是 LLVM 级别的工业后端
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 7. 与 `lecture11` 的对应关系
|
|
|
|
|
## 7. 调用约定与栈布局
|
|
|
|
|
|
|
|
|
|
这部分是当前实现与参考资料对齐程度最高的一部分。
|
|
|
|
|
### 7.1 与 `lab03` 的关系
|
|
|
|
|
|
|
|
|
|
在 [src/mir/RegAlloc.cpp](/home/hw/nudt-compiler-cpp/src/mir/RegAlloc.cpp:166) 中,当前使用 `GeorgeColoringAllocator` 完成寄存器分配。核心步骤包括:
|
|
|
|
|
`lab03` 的重点是:
|
|
|
|
|
|
|
|
|
|
1. 基于基本块的 `use/def/live_in/live_out` 活跃性分析
|
|
|
|
|
2. 构建干涉图
|
|
|
|
|
3. 收集 `Copy` 指令形成 move 关系
|
|
|
|
|
4. `simplify`
|
|
|
|
|
5. `coalesce`
|
|
|
|
|
6. `freeze`
|
|
|
|
|
7. `select spill`
|
|
|
|
|
8. `assign colors`
|
|
|
|
|
9. spill 槽分配与最终 `Allocation` 提交
|
|
|
|
|
- 正确的 AArch64 / AAPCS64 调用约定
|
|
|
|
|
- 正确的栈帧构造
|
|
|
|
|
- 16 字节对齐
|
|
|
|
|
- caller-saved 与 callee-saved 的区分
|
|
|
|
|
|
|
|
|
|
这和 `lecture11` 中 George 改进算法的讲义结构是一致的。
|
|
|
|
|
当前仓库在这些点上总体是符合的。
|
|
|
|
|
|
|
|
|
|
当前具体策略还包括:
|
|
|
|
|
### 7.2 当前调用约定实现
|
|
|
|
|
|
|
|
|
|
- GPR / FPR 分开着色
|
|
|
|
|
- spill cost 按基本块权重估计
|
|
|
|
|
- 被分配到 callee-saved 的物理寄存器会记录到函数对象中,供后续序言 / 尾声保存恢复
|
|
|
|
|
参数与返回值规则主要由 [src/mir/AsmPrinter.cpp](../src/mir/AsmPrinter.cpp) 负责落地。
|
|
|
|
|
|
|
|
|
|
因此,寄存器分配部分可以判断为:
|
|
|
|
|
当前已经实现:
|
|
|
|
|
|
|
|
|
|
- 不只是“概念上参考 George”
|
|
|
|
|
- 而是代码结构上已经明显按 George 图着色路线实现
|
|
|
|
|
- 整型参数优先使用 `x0-x7`
|
|
|
|
|
- 浮点参数优先使用 `s0-s7`
|
|
|
|
|
- 超出寄存器容量的参数走栈
|
|
|
|
|
- 整型返回值走 `x0`
|
|
|
|
|
- 浮点返回值走 `s0`
|
|
|
|
|
- 调用前按需要扩栈,调用后回收
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
形参接收通过 `MachineInstr::Arg` 发射,调用点搬参与返回值接收通过 `MachineInstr::Call` 发射。
|
|
|
|
|
|
|
|
|
|
## 8. 栈布局实现说明
|
|
|
|
|
### 7.3 当前栈对象来源
|
|
|
|
|
|
|
|
|
|
当前栈对象来源主要有三类:
|
|
|
|
|
栈对象主要来自三类:
|
|
|
|
|
|
|
|
|
|
- `alloca` 降低得到的局部对象
|
|
|
|
|
- 寄存器分配产生的 spill 槽
|
|
|
|
|
- 需要保存的 callee-saved 寄存器槽
|
|
|
|
|
- 被使用到的 callee-saved 寄存器保存槽
|
|
|
|
|
|
|
|
|
|
### 7.4 当前帧布局方式
|
|
|
|
|
|
|
|
|
|
布局过程在 [src/mir/FrameLowering.cpp](/home/hw/nudt-compiler-cpp/src/mir/FrameLowering.cpp:1) 中完成,基本策略为:
|
|
|
|
|
在 [src/mir/FrameLowering.cpp](../src/mir/FrameLowering.cpp) 中,当前布局策略为:
|
|
|
|
|
|
|
|
|
|
1. 遍历栈对象列表。
|
|
|
|
|
2. 按对象对齐要求推进 `cursor`。
|
|
|
|
|
3. 将对象偏移记为相对 `x29` 的负偏移。
|
|
|
|
|
4. 最终将 `frame_size` 向上对齐到 16 字节。
|
|
|
|
|
1. 遍历所有栈对象
|
|
|
|
|
2. 按对象对齐要求推进 `cursor`
|
|
|
|
|
3. 记录相对帧指针的对象偏移
|
|
|
|
|
4. 将最终 `frame_size` 向上对齐到 16 字节
|
|
|
|
|
|
|
|
|
|
汇编层使用方式如下:
|
|
|
|
|
在汇编发射时:
|
|
|
|
|
|
|
|
|
|
- 栈对象地址通过 `EmitFrameAddress` 生成。
|
|
|
|
|
- spill load/store 使用 `x17` 作为内部临时地址寄存器。
|
|
|
|
|
- 序言中先保存 `x29/x30`,再下移 `sp`,再保存实际使用到的 callee-saved 寄存器。
|
|
|
|
|
- `x29` 作为帧指针
|
|
|
|
|
- `x30` 作为返回地址寄存器
|
|
|
|
|
- 需要保存的 callee-saved GPR/FPR 会出现在序言和尾声中
|
|
|
|
|
- spill/load/store 通过统一的帧地址访问例程发射
|
|
|
|
|
|
|
|
|
|
这与 `lab03` 文档中的 ARMv8 栈帧思路是一致的。
|
|
|
|
|
### 7.5 当前寄存器选择策略对调用的影响
|
|
|
|
|
|
|
|
|
|
当前寄存器分配器对 GPR 主要使用 `x19-x28`,对 FPR 主要使用 `s8-s15`。这是一种偏保守但稳定的策略,优点是:
|
|
|
|
|
|
|
|
|
|
- 调用边界更容易处理
|
|
|
|
|
- caller-saved 污染更少
|
|
|
|
|
- 实现复杂度低
|
|
|
|
|
|
|
|
|
|
代价是:
|
|
|
|
|
|
|
|
|
|
- 可分配寄存器集合比 LLVM 更小
|
|
|
|
|
- 高压代码里更容易 spill
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 9. 调用约定实现说明
|
|
|
|
|
## 8. George 图着色寄存器分配
|
|
|
|
|
|
|
|
|
|
当前 AArch64 调用约定的落地主要在 [src/mir/AsmPrinter.cpp](/home/hw/nudt-compiler-cpp/src/mir/AsmPrinter.cpp:843) 之后。
|
|
|
|
|
这部分与 `lecture11` 的对应关系最强。
|
|
|
|
|
|
|
|
|
|
已经实现的规则包括:
|
|
|
|
|
在 [src/mir/RegAlloc.cpp](../src/mir/RegAlloc.cpp) 中,当前实现包含以下典型步骤:
|
|
|
|
|
|
|
|
|
|
- 整型参数优先使用 `x0-x7`
|
|
|
|
|
- 浮点参数优先使用 `v0-v7`
|
|
|
|
|
- 超过寄存器容量的参数按栈传递
|
|
|
|
|
- 整型返回值走 `x0`
|
|
|
|
|
- 浮点返回值走 `s0`
|
|
|
|
|
- 栈上传参调用前先扩栈,调用后回收
|
|
|
|
|
1. 基本块级 `use/def/live_in/live_out` 活跃性分析
|
|
|
|
|
2. 干涉图构建
|
|
|
|
|
3. `Copy` 指令诱导的 move 关系收集
|
|
|
|
|
4. `simplify`
|
|
|
|
|
5. `coalesce`
|
|
|
|
|
6. `freeze`
|
|
|
|
|
7. `select spill`
|
|
|
|
|
8. `assign colors`
|
|
|
|
|
9. spill 槽创建与最终 `Allocation` 提交
|
|
|
|
|
|
|
|
|
|
当前设计中:
|
|
|
|
|
当前实现还有几个重要特征:
|
|
|
|
|
|
|
|
|
|
- 参数位置计算由 `ComputeArgLocation` 完成
|
|
|
|
|
- 调用点参数搬运在 `MachineInstr::Call` 的汇编发射中完成
|
|
|
|
|
- 形参读取在 `MachineInstr::Arg` 的汇编发射中完成
|
|
|
|
|
- GPR 和 FPR 分开着色
|
|
|
|
|
- spill cost 会参考基本块权重
|
|
|
|
|
- 分配到 callee-saved 的物理寄存器会记录回函数对象,供后续序言和尾声保存恢复
|
|
|
|
|
|
|
|
|
|
整体上已经满足 Lab3 基础调用约定要求。
|
|
|
|
|
因此,这里不是“概念上参考了图着色”,而是代码结构上就已经沿着 George 算法在实现。
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 10. 近期关键修正
|
|
|
|
|
## 9. 近期关键正确性修复
|
|
|
|
|
|
|
|
|
|
### 9.1 `phi` 并行 copy 修复
|
|
|
|
|
|
|
|
|
|
修复位置:
|
|
|
|
|
|
|
|
|
|
- [src/mir/Lowering.cpp](../src/mir/Lowering.cpp)
|
|
|
|
|
|
|
|
|
|
原始问题是:多个 `phi copy` 被按普通顺序赋值发射,旧值可能在后续 copy 使用前就被提前覆盖。
|
|
|
|
|
|
|
|
|
|
这在复杂循环头里会表现为:
|
|
|
|
|
|
|
|
|
|
近期最重要的 Lab3 正确性修正发生在 [src/mir/Lowering.cpp](/home/hw/nudt-compiler-cpp/src/mir/Lowering.cpp:1) 的 `phi` lowering。
|
|
|
|
|
- `a' <- t`
|
|
|
|
|
- `b' <- a`
|
|
|
|
|
- `d' <- c`
|
|
|
|
|
- `e' <- d`
|
|
|
|
|
|
|
|
|
|
### 10.1 修正前的问题
|
|
|
|
|
如果先发 `a' <- t`,后面的 `b' <- a` 读到的就不是旧 `a`,而是已经被覆盖的新值。
|
|
|
|
|
|
|
|
|
|
原先 `phi` lowering` 会把前驱到后继的 copy 当作普通顺序赋值处理。
|
|
|
|
|
在复杂循环头中,多个 `phi` 之间存在“旧值仍要被后续 copy 使用”的情况,如果拷贝顺序错误,就会提前覆盖旧值,生成错误结果。
|
|
|
|
|
当前修复后的策略是:
|
|
|
|
|
|
|
|
|
|
这一问题直接导致了:
|
|
|
|
|
- 先按 CFG 边收集所有 `phi copy`
|
|
|
|
|
- 优先发“目的寄存器不再被其他待发 copy 当作源使用”的 copy
|
|
|
|
|
- 如有环,则引入临时 vreg 打破
|
|
|
|
|
- 对条件边在必要时插入专用边块
|
|
|
|
|
|
|
|
|
|
这个问题直接导致过:
|
|
|
|
|
|
|
|
|
|
- `crypto-1.sy`
|
|
|
|
|
- `crypto-2.sy`
|
|
|
|
|
- `crypto-3.sy`
|
|
|
|
|
|
|
|
|
|
三个高性能样例的汇编结果错误,而对应 IR 结果是正确的。
|
|
|
|
|
修复后,这三个样例已经恢复通过。
|
|
|
|
|
|
|
|
|
|
### 9.2 有序浮点比较的 NaN 语义修复
|
|
|
|
|
|
|
|
|
|
修复位置:
|
|
|
|
|
|
|
|
|
|
- [src/mir/AsmPrinter.cpp](../src/mir/AsmPrinter.cpp)
|
|
|
|
|
|
|
|
|
|
这个问题比表面上看起来更隐蔽。IR 层的浮点比较打印是:
|
|
|
|
|
|
|
|
|
|
- `FCmpEQ -> fcmp oeq`
|
|
|
|
|
- `FCmpNE -> fcmp one`
|
|
|
|
|
- `FCmpLT -> fcmp olt`
|
|
|
|
|
- `FCmpGT -> fcmp ogt`
|
|
|
|
|
- `FCmpLE -> fcmp ole`
|
|
|
|
|
- `FCmpGE -> fcmp oge`
|
|
|
|
|
|
|
|
|
|
见 [src/ir/IRPrinter.cpp](../src/ir/IRPrinter.cpp)。
|
|
|
|
|
|
|
|
|
|
这里的关键字是 `ordered`。也就是说,比较一旦遇到 `NaN`,这些条件不应该按普通整数式条件码去理解。
|
|
|
|
|
|
|
|
|
|
原来的 Lab3 后端把 `FCmp` 的结果物化和 `FCmp + CondBr` 融合分支都简单映射成了:
|
|
|
|
|
|
|
|
|
|
- `eq/ne/lt/gt/le/ge`
|
|
|
|
|
|
|
|
|
|
这会在 AArch64 上引入错误的 `NaN` 语义。对照 LLVM AArch64 后端后,当前修正为:
|
|
|
|
|
|
|
|
|
|
- `oeq -> eq`
|
|
|
|
|
- `olt -> mi`
|
|
|
|
|
- `ogt -> gt`
|
|
|
|
|
- `ole -> ls`
|
|
|
|
|
- `oge -> ge`
|
|
|
|
|
- `one -> 复合逻辑,不是单一条件码`
|
|
|
|
|
|
|
|
|
|
也就是说,浮点比较不能直接照抄整数比较的条件码名称。
|
|
|
|
|
|
|
|
|
|
### 9.3 `vector_mul3` 超时的真实原因
|
|
|
|
|
|
|
|
|
|
`vector_mul3` 最开始表现为超时,很容易误判成:
|
|
|
|
|
|
|
|
|
|
- 热点循环代码生成太慢
|
|
|
|
|
- spill 太多
|
|
|
|
|
- 指令选择不够激进
|
|
|
|
|
|
|
|
|
|
但实际定位后发现,真正原因不是主循环慢,而是浮点比较语义错了。
|
|
|
|
|
|
|
|
|
|
### 10.2 修正内容
|
|
|
|
|
定位过程中的关键事实有两点:
|
|
|
|
|
|
|
|
|
|
当前 `phi` lowering` 已改为:
|
|
|
|
|
- Lab2 全量 `214 PASS / 0 FAIL`
|
|
|
|
|
- `vector_mul3` 在 Lab2 不超时
|
|
|
|
|
|
|
|
|
|
- 先按边收集 `phi` copy`
|
|
|
|
|
- 对条件跳转前驱建立专门的边块
|
|
|
|
|
- 按并行 copy 规则发射
|
|
|
|
|
- 优先发“目的寄存器不再被其他 copy 当作源使用”的 copy
|
|
|
|
|
- 如果形成环,则使用临时 vreg 打破
|
|
|
|
|
对应日志见 [output/logs/lab2/lab2_20260412_183222/whole.log](../output/logs/lab2/lab2_20260412_183222/whole.log)。
|
|
|
|
|
|
|
|
|
|
这个修正已经使 `crypto-1/2/3` 三个样例重新通过。
|
|
|
|
|
这说明:
|
|
|
|
|
|
|
|
|
|
- 算法本身并不必然超时
|
|
|
|
|
- 前端、语义和 IR 也不是根因
|
|
|
|
|
- 真正问题在 Lab3 后端生成的汇编语义
|
|
|
|
|
|
|
|
|
|
进一步缩小后发现:
|
|
|
|
|
|
|
|
|
|
- `vector_mul3` 的主循环和点积本身能够结束
|
|
|
|
|
- 真正卡住的是 `my_sqrt`
|
|
|
|
|
- 根因是 `my_sqrt` 在输入为 `NaN` 时,循环条件被后端错误判真,导致死循环
|
|
|
|
|
|
|
|
|
|
因此,这不是“性能优化不够”的问题,而是“浮点有序比较语义错误导致的超时型正确性 bug”。
|
|
|
|
|
|
|
|
|
|
修复后,`vector_mul3` 已正常通过。
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 11. 当前测试状态
|
|
|
|
|
## 10. 测试脚本与日志规则
|
|
|
|
|
|
|
|
|
|
当前 Lab3 的批量测试脚本为:
|
|
|
|
|
Lab3 当前使用的脚本为:
|
|
|
|
|
|
|
|
|
|
- [scripts/lab3_build_test.sh](/home/hw/nudt-compiler-cpp/scripts/lab3_build_test.sh:1)
|
|
|
|
|
- [scripts/verify_asm.sh](/home/hw/nudt-compiler-cpp/scripts/verify_asm.sh:1)
|
|
|
|
|
- [scripts/lab3_build_test.sh](../scripts/lab3_build_test.sh)
|
|
|
|
|
- [scripts/verify_asm.sh](../scripts/verify_asm.sh)
|
|
|
|
|
|
|
|
|
|
测试规则为:
|
|
|
|
|
测试规则已经固定为:
|
|
|
|
|
|
|
|
|
|
- 每次运行生成独立目录 `lab3_日期_时间`
|
|
|
|
|
- 每次运行生成独立目录 `output/logs/lab3/lab3_YYYYMMDD_HHMMSS/`
|
|
|
|
|
- 目录中保留完整 `whole.log`
|
|
|
|
|
- 成功样例的中间文件自动删除
|
|
|
|
|
- 失败样例保留中间目录与 `error.log`
|
|
|
|
|
- 成功样例中间文件自动删除
|
|
|
|
|
- 失败样例保留中间目录
|
|
|
|
|
- 每个失败样例目录必须包含 `error.log`
|
|
|
|
|
|
|
|
|
|
也就是说,当前脚本已经符合“只保留失败用例中间文件”的要求。
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 11. 当前测试结果
|
|
|
|
|
|
|
|
|
|
### 11.1 `crypto-*` 修复后的失败集复查
|
|
|
|
|
|
|
|
|
|
在先修完 `phi` lowering 后,失败集复查日志为:
|
|
|
|
|
|
|
|
|
|
- [output/logs/lab3/lab3_20260412_143811/whole.log](../output/logs/lab3/lab3_20260412_143811/whole.log)
|
|
|
|
|
|
|
|
|
|
最新一次失败集复查日志位于:
|
|
|
|
|
当时的结果是:
|
|
|
|
|
|
|
|
|
|
- [output/logs/lab3/lab3_20260412_143811/whole.log](/home/hw/nudt-compiler-cpp/output/logs/lab3/lab3_20260412_143811/whole.log:1)
|
|
|
|
|
- `crypto-1.sy` 通过
|
|
|
|
|
- `crypto-2.sy` 通过
|
|
|
|
|
- `crypto-3.sy` 通过
|
|
|
|
|
- `vector_mul3.sy` 仍失败
|
|
|
|
|
|
|
|
|
|
当前失败集复查结果为:
|
|
|
|
|
这一步证明 `crypto-*` 的根因确实在 `phi` 并行 copy。
|
|
|
|
|
|
|
|
|
|
- `PASS test/test_case/h_performance/crypto-1.sy`
|
|
|
|
|
- `PASS test/test_case/h_performance/crypto-2.sy`
|
|
|
|
|
- `PASS test/test_case/h_performance/crypto-3.sy`
|
|
|
|
|
- `FAIL test/class_test_case/performance/vector_mul3.sy`
|
|
|
|
|
### 11.2 `vector_mul3` 修复后的单项复查
|
|
|
|
|
|
|
|
|
|
也就是说,当前 Lab3 后端已经解决了前期的 `crypto-*` 正确性问题,但仍然保留一个性能型尾项:
|
|
|
|
|
只重跑失败集时,日志为:
|
|
|
|
|
|
|
|
|
|
- `vector_mul3.sy`
|
|
|
|
|
- [output/logs/lab3/lab3_20260412_185610/whole.log](../output/logs/lab3/lab3_20260412_185610/whole.log)
|
|
|
|
|
|
|
|
|
|
该样例当前表现为超时,而不是输出错误。
|
|
|
|
|
结果为:
|
|
|
|
|
|
|
|
|
|
- `vector_mul3.sy` 通过
|
|
|
|
|
|
|
|
|
|
这一步证明浮点比较修复已经消除了剩余尾项。
|
|
|
|
|
|
|
|
|
|
### 11.3 最新 Lab3 全量结果
|
|
|
|
|
|
|
|
|
|
最新全量运行日志为:
|
|
|
|
|
|
|
|
|
|
- [output/logs/lab3/lab3_20260412_185655/whole.log](../output/logs/lab3/lab3_20260412_185655/whole.log)
|
|
|
|
|
|
|
|
|
|
全量结果为:
|
|
|
|
|
|
|
|
|
|
- `214 PASS / 0 FAIL / total 214`
|
|
|
|
|
|
|
|
|
|
因此,当前 Lab3 后端在现有测试集上已经全部通过。
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 12. 当前结论
|
|
|
|
|
|
|
|
|
|
综合来看,当前 Lab3 后端可以准确概括为:
|
|
|
|
|
综合来看,当前项目中的 Lab3 后端可以准确概括为:
|
|
|
|
|
|
|
|
|
|
- 已经完成自研 MIR 后端主链路
|
|
|
|
|
- 栈布局与调用约定总体符合 `lab03`
|
|
|
|
|
- 指令选择符合 `lecture05` 的宏扩展与局部模式优化思路,但不是完整树匹配版本
|
|
|
|
|
- 寄存器分配高度符合 `lecture11` 的 George 图着色路线
|
|
|
|
|
- `crypto-*` 正确性问题已经修复
|
|
|
|
|
- 仍保留 `vector_mul3` 一个性能型尾项
|
|
|
|
|
- `phi` 并行 copy 正确性问题已经修复
|
|
|
|
|
- 有序浮点比较的 NaN 语义问题已经修复
|
|
|
|
|
- `crypto-*` 和 `vector_mul3` 均已通过
|
|
|
|
|
- 最新 Lab3 全量测试结果为 `214 PASS / 0 FAIL`
|
|
|
|
|
|
|
|
|
|
因此,当前更准确的表述已经不是“Lab3 框架基本成型”,而是:
|
|
|
|
|
|
|
|
|
|
- Lab3 后端功能链路已经完整
|
|
|
|
|
- 当前测试集下正确性已经收敛
|
|
|
|
|
- 实现风格清晰地对应 `lab03 + lecture05 + lecture11`
|
|
|
|
|
|
|
|
|
|
如果后续继续做优化,重点就不再是“修正明显错误”,而是:
|
|
|
|
|
|
|
|
|
|
因此,当前项目最准确的表述不是“Lab3 完全结束”,而是:
|
|
|
|
|
- 提升生成代码质量
|
|
|
|
|
- 扩大可分配寄存器利用范围
|
|
|
|
|
- 增加更强的机器相关优化
|
|
|
|
|
|
|
|
|
|
- Lab3 的代码生成框架已经成型并基本可用
|
|
|
|
|
- 正确性主体已经收敛
|
|
|
|
|
- 仍有少量性能尾项待继续优化
|
|
|
|
|
但这些属于后续优化方向,不影响当前 Lab3 已经完成并通过现有测试集这一结论。
|
|
|
|
|
|