Lab5: Implement register-aliasing-aware peephole optimization pass for redundant stack instruction elimination

This commit is contained in:
2026-05-18 14:30:22 +08:00
committed by CGH0S7
parent 8f7e0ac5b4
commit 4475e91bd8
4 changed files with 277 additions and 3 deletions

91
doc/Lab5-实验记录.md Normal file
View File

@@ -0,0 +1,91 @@
# Lab5 实验记录:寄存器分配与后端窥孔优化
## 1. 实验目标
本次 Lab5 的核心目标是在已有的中间表示生成与汇编生成框架基础上,实现高效的寄存器分配与后端优化技术。
本次完成工作的重点包括:
- 在汇编代码生成AArch64的框架下理解并适配从虚拟寄存器到物理寄存器的分配管理Linear Scan 或基本图着色)。
- 实现后端窥孔优化Peephole Optimization消除冗余的寄存器 move 指令(如 `mov w8, w8`)和多余的栈加载/存储指令(如 redundant Load-after-Store
- 处理 AArch64 寄存器别名W 寄存器与 X 寄存器)以及浮点/通用寄存器的交互边界,解决浮点常数加载的副作用。
- 通过全面的功能测试套件(`verify_asm.sh`)以保证生成的汇编在 QEMU 模拟器环境下的正确运行。
## 2. 代码改动范围
本次实验主要涉及和修改了以下模块:
- `include/mir/MIR.h`:增加 `RunPeephole` 优化通路的函数声明。
- `src/mir/passes/Peephole.cpp`:实现完整的后端窥孔优化处理器,包括寄存器尺度匹配、寄存器别名正规化以及栈读写冗余消除。
- `src/main.cpp`:将后端优化入口 `RunPeephole` 插入到汇编生成的整个管线中。
- 新增文档:`doc/Lab5-实验记录.md`
## 3. 完成过程
### 3.1 问题边界定位与痛点分析
在进行后端优化与窥孔之前,编译器能够正常输出 AArch64 汇编。但是由于寄存器分配和栈槽管理的保守性,生成的汇编代码中充斥着大量的:
1. 冗余的同名寄存器 self-move`mov w9, w9``mov x8, x8`)。
2. 在溢出与重载场景中,大量的 `StoreStack` 后紧跟 `LoadStack` 到相同物理寄存器的冗余操作。
3. 浮点数常量在 AArch64 后端加载时,通常需要通过常数池(`adrp` + `ldr`)加载,在此过程中需要临时占用通用寄存器(如 `x8`/`w8`)。
如果窥孔优化对 AArch64 的通用寄存器别名Wn 对应 Xn 的低 32 位)和隐式寄存器改写认知不够清晰,就会导致错误的优化,使得浮点数表达式比较时生成错误的汇编,进而在 QEMU 中引发 Segment Fault 或结果不匹配。
### 3.2 窥孔优化的具体设计与实现
为了保证性能与正确性,本实验在 `src/mir/passes/Peephole.cpp` 中设计了基于数据流上下文的单块窥孔扫描机制:
1. **同名物理寄存器正规化NormalizeReg**
AArch64 下,`W0``W28``X0``X28` 是一对一重叠映射的。在做跟踪和消除 redundant Load-after-Store 时,必须将 64 位寄存器统一转换为 32 位别名正规化处理避免因为指令尺寸不同W vs X导致寄存器别名追踪失效。
2. **寄存器大小动态适配MatchRegSize**
在做 `LoadStack` 替换为 `MovReg` 时,如果源寄存器是 64 位的(如 X9而目标寄存器是 32 位的(如 W0不能直接生成 `mov w0, x9`。必须调用 `MatchRegSize` 动态判断并裁剪为相同尺寸的 `mov w0, w9`,确保生成的汇编指令能够通过 GNU 汇编器编译。
3. **隐式写寄存器的追踪**
识别后端中隐式读写 `x8`/`w8` 临时寄存器的指令(例如浮点 `MovImm`),并在窥孔器扫描到此类指令时,主动失效被覆盖寄存器的活动跟踪状态,解决由此导致的寄存器污染问题。
## 4. 关键困难与解决办法
### 4.1 困难一:浮点常数隐式加载改写寄存器的副作用
#### 现象
在浮点测试用例 `95_float.sy` 进行编译时,发现部分浮点比较的结果不正确。经跟踪发现,浮点 `MovImm` 最终会被翻译为通过 PC 相对寻址(`adrp` + `ldr`)加载 `rodata`,该过程会隐式使用通用寄存器 `x8`/`w8`,而这会破坏正在被跟踪的 `x8`/`w8` 值。
#### 解决办法
`Peephole.cpp` 的指令写失效扫描逻辑中,显式识别 `MovImm` 的目标寄存器类型。如果目标寄存器是浮点寄存器(`S0` - `S15`),我们主动将 `slot_to_reg` 追踪关系中的 `x8`/`w8` 条目全部擦除失效。
#### 效果
隐式写寄存器失效策略完全排除了因常数池加载造成的寄存器污染问题,浮点计算和浮点比较指令行为变得绝对正确。
### 4.2 困难二W 寄存器与 X 寄存器别名判定失误
#### 现象
在汇编生成时,可能会对同一个物理寄存器先后用 32 位和 64 位名称引用,如先 `str w8, [sp]`,后 `ldr x8, [sp]`。如果直接用简单的字符串比对或物理寄存器枚举值比对,会认为这是两个不相关的寄存器。
#### 解决办法
引入了 `NormalizeReg`:将所有的 64 位通用寄存器 `X0`-`X28` 归一化映射到其对应的 32 位别名 `W0`-`W28`。所有的别名冲突、冗余自移动消除Self-move elimination均基于归一化后的寄存器进行。
## 5. 验证结果
`lab5` 编译优化管线加入后,运行:
```bash
./scripts/verify_asm.sh test/test_case/functional/95_float.sy --run
```
退出码:`0`,输出完全匹配期望。
另外,对全部的 functional 样例执行回归测试:
```bash
for f in test/test_case/functional/*.sy; do
./scripts/verify_asm.sh "$f" --run
done
```
验证结果表明:**所有 functional 样例在窥孔优化开启后,均成功编译生成汇编、链接并完美运行,退出状态码与标准输出完全符合预期。**
## 6. 实验总结与后续工作
本次后端窥孔优化大幅缩减了物理汇编代码中冗余的栈读写指令和同名自拷贝指令,提高了生成代码的紧凑程度与执行效率。
后续可在当前工作的基础上,进一步在 Lab6 中打通更高级的循环不变式外提LICM等前端与中端的高级循环优化技术。

View File

@@ -153,6 +153,7 @@ class MachineFunction {
std::vector<std::unique_ptr<MachineFunction>> LowerToMIR(const ir::Module& module); std::vector<std::unique_ptr<MachineFunction>> LowerToMIR(const ir::Module& module);
void RunRegAlloc(MachineFunction& function); void RunRegAlloc(MachineFunction& function);
void RunFrameLowering(MachineFunction& function); void RunFrameLowering(MachineFunction& function);
void RunPeephole(MachineFunction& function);
void PrintAsm(const MachineFunction& function, std::ostream& os); void PrintAsm(const MachineFunction& function, std::ostream& os);
void PrintGlobals(const ir::Module& module, std::ostream& os); void PrintGlobals(const ir::Module& module, std::ostream& os);

View File

@@ -53,6 +53,7 @@ int main(int argc, char** argv) {
for (auto& machine_func : machine_funcs) { for (auto& machine_func : machine_funcs) {
mir::RunRegAlloc(*machine_func); mir::RunRegAlloc(*machine_func);
mir::RunFrameLowering(*machine_func); mir::RunFrameLowering(*machine_func);
mir::RunPeephole(*machine_func);
if (need_blank_line) { if (need_blank_line) {
std::cout << "\n"; std::cout << "\n";
} }

View File

@@ -1,4 +1,185 @@
// 窥孔优化Peephole #include "mir/MIR.h"
// - 删除冗余 move、合并常见指令模式 #include <unordered_map>
// - 提升最终汇编质量(按实现范围裁剪) #include <vector>
namespace mir {
namespace {
PhysReg NormalizeReg(PhysReg reg) {
int r = static_cast<int>(reg);
// Map 64-bit X0-X28 registers to 32-bit W0-W28 registers to handle aliasing
if (r >= static_cast<int>(PhysReg::X0) && r <= static_cast<int>(PhysReg::X28)) {
return static_cast<PhysReg>(r - static_cast<int>(PhysReg::X0) + static_cast<int>(PhysReg::W0));
}
return reg;
}
PhysReg MatchRegSize(PhysReg target, PhysReg src) {
int t = static_cast<int>(target);
int s = static_cast<int>(src);
bool target_is_64 = (t >= static_cast<int>(PhysReg::X0) && t <= static_cast<int>(PhysReg::X28)) ||
t == static_cast<int>(PhysReg::X29) ||
t == static_cast<int>(PhysReg::X30) ||
t == static_cast<int>(PhysReg::SP);
bool src_is_64 = (s >= static_cast<int>(PhysReg::X0) && s <= static_cast<int>(PhysReg::X28)) ||
s == static_cast<int>(PhysReg::X29) ||
s == static_cast<int>(PhysReg::X30) ||
s == static_cast<int>(PhysReg::SP);
if (target_is_64 && !src_is_64) {
if (s >= static_cast<int>(PhysReg::W0) && s <= static_cast<int>(PhysReg::W28)) {
return static_cast<PhysReg>(s - static_cast<int>(PhysReg::W0) + static_cast<int>(PhysReg::X0));
}
} else if (!target_is_64 && src_is_64) {
if (s >= static_cast<int>(PhysReg::X0) && s <= static_cast<int>(PhysReg::X28)) {
return static_cast<PhysReg>(s - static_cast<int>(PhysReg::X0) + static_cast<int>(PhysReg::W0));
}
}
return src;
}
bool IsFloatReg(PhysReg reg) {
return reg >= PhysReg::S0 && reg <= PhysReg::S15;
}
} // namespace
void RunPeephole(MachineFunction& function) {
for (auto& block : function.GetBlocks()) {
auto& insts = block.GetInstructions();
std::vector<MachineInstr> optimized;
// Map from FrameIndex to the normalized physical register that currently holds its value
std::unordered_map<int, PhysReg> slot_to_reg;
for (const auto& inst : insts) {
Opcode op = inst.GetOpcode();
const auto& ops = inst.GetOperands();
// 1. Handle register move elimination (e.g. mov w8, w8)
if (op == Opcode::MovReg) {
if (NormalizeReg(ops.at(0).GetReg()) == NormalizeReg(ops.at(1).GetReg())) {
continue; // Delete redundant self-moves
}
}
// 2. Handle redundant Load after Store
if (op == Opcode::LoadStack) {
int fi = ops.at(1).GetFrameIndex();
auto it = slot_to_reg.find(fi);
if (it != slot_to_reg.end()) {
PhysReg source_reg = it->second;
PhysReg dest_reg = NormalizeReg(ops.at(0).GetReg());
if (source_reg == dest_reg) {
// Loading the same register that already has the value - completely redundant!
continue;
} else {
// Replace LoadStack dest_reg, fi with MovReg dest_reg, matched_source
PhysReg matched_source = MatchRegSize(ops.at(0).GetReg(), it->second);
optimized.push_back(MachineInstr(Opcode::MovReg, {Operand::Reg(ops.at(0).GetReg()), Operand::Reg(matched_source)}));
// Invalidate any other slots mapping to dest_reg because dest_reg is written
std::vector<int> to_remove;
for (const auto& pair : slot_to_reg) {
if (NormalizeReg(pair.second) == dest_reg) {
to_remove.push_back(pair.first);
}
}
for (int key : to_remove) {
slot_to_reg.erase(key);
}
// Add new mapping (normalized)
slot_to_reg[fi] = dest_reg;
continue;
}
}
}
// 3. Track stores
if (op == Opcode::StoreStack) {
PhysReg src = NormalizeReg(ops.at(0).GetReg());
int fi = ops.at(1).GetFrameIndex();
slot_to_reg[fi] = src;
}
// 4. Invalidate register mappings on writes
bool writes_reg = false;
PhysReg written_reg = PhysReg::W0; // dummy
switch (op) {
case Opcode::MovImm:
if (!ops.empty() && ops.at(0).GetKind() == Operand::Kind::Reg) {
writes_reg = true;
written_reg = NormalizeReg(ops.at(0).GetReg());
// Under the hood, MovImm to a float register implicitly writes to x8/w8
if (IsFloatReg(ops.at(0).GetReg())) {
PhysReg implicitly_written = NormalizeReg(PhysReg::X8);
std::vector<int> to_remove;
for (const auto& pair : slot_to_reg) {
if (NormalizeReg(pair.second) == implicitly_written) {
to_remove.push_back(pair.first);
}
}
for (int key : to_remove) {
slot_to_reg.erase(key);
}
}
}
break;
case Opcode::LoadStack:
case Opcode::AddRR:
case Opcode::SubRR:
case Opcode::MulRR:
case Opcode::SDivRR:
case Opcode::MSubRRRR:
case Opcode::FAddRRR:
case Opcode::FSubRRR:
case Opcode::FMulRRR:
case Opcode::FDivRRR:
case Opcode::Cset:
case Opcode::MovReg:
case Opcode::Adrp:
case Opcode::AddRegImm:
case Opcode::LdrRegReg:
case Opcode::SIToFP:
case Opcode::FPToSI:
case Opcode::ZExt:
if (!ops.empty() && ops.at(0).GetKind() == Operand::Kind::Reg) {
writes_reg = true;
written_reg = NormalizeReg(ops.at(0).GetReg());
}
break;
case Opcode::Call:
// A function call destroys all temporary/scratch registers.
slot_to_reg.clear();
break;
default:
break;
}
if (writes_reg) {
// Remove any slot mapping to this register
std::vector<int> to_remove;
for (const auto& pair : slot_to_reg) {
if (NormalizeReg(pair.second) == written_reg) {
to_remove.push_back(pair.first);
}
}
for (int key : to_remove) {
slot_to_reg.erase(key);
}
}
optimized.push_back(inst);
}
insts = std::move(optimized);
}
}
} // namespace mir