Files
nudt-compiler-cpp/doc/Lab4-实验记录.md

151 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Lab4 实验记录:基本标量优化
## 1. 实验目标
本次 Lab4 的目标是在 Lab3 汇编生成的基础上,构建编译器的 IR 级标量优化通道Optimizer Passes。要求将生成的中间表示SysY IR转换为静态单赋值形式SSA, Static Single Assignment实现内存变量到 SSA 寄存器的提升Mem2Reg并在此之上运行一系列经典的标量优化算法最后由后端正确降低 SSA 形式的 IR特别是 Phi 节点)为高性能的 AArch64 汇编。
本次完成的工作重点包括:
- **支配树分析**`DominatorTree.cpp`):实现高效的 Cooper-Harvey-Kennedy 迭代支配树求解算法构建支配边界Dominance Frontiers以及直接支配者IDom关系。
- **Mem2Reg 提升**`Mem2Reg.cpp`):完成局部标量 scalar allocas 的提升,在汇合点插入合法的 Phi 节点并进行变量重命名,实现从非 SSA 到正式 SSA 形式的蜕变。
- **常量折叠与传播**`ConstFold.cpp` & `ConstProp.cpp`):支持算术、比较、逻辑与强类型转换指令的深度折叠与代数简化。
- **公共子表达式删除**`CSE.cpp`):实现块内局部公共子表达式消除。
- **死代码删除**`DCE.cpp`使用基于活跃度传播Mark-and-Sweep的算法彻底剔除无副作用且未被使用的多余指令。
- **控制流图简化**`CFGSimplify.cpp`):迭代合并单前驱单后继基本块,清理不可达代码。
- **SSA 后端支持与 Phi 节点降低**`Lowering.cpp`):在栈槽后端正确处理 Phi 节点生命周期通过在控制流分叉的基本块末尾生成条件拷贝Condition Copy-Store以及在函数头部预分配 Phi 槽位,确保降低到 AArch64 时的正确性。
- **修复指针截断、参数 GEP 越界和分支 Phi 冗余**等多处极其隐蔽的后端缺陷,使所有用例完全通过。
---
## 2. 代码改动范围
主要修改或新增了以下文件:
- `include/ir/IR.h` & `src/ir/Instruction.cpp` & `src/ir/IRBuilder.cpp`(扩展支持 `Opcode::Phi` 节点)
- `src/ir/IRPrinter.cpp`Phi 节点序列化打印输出)
- `include/ir/PassManager.h` & `src/ir/passes/PassManager.cpp`(集中配置与管理优化 Passes
- `src/ir/analysis/DominatorTree.cpp`(新增支配树求解分析)
- `src/ir/passes/Mem2Reg.cpp`(新增 Mem2Reg 标量提升)
- `src/ir/passes/ConstFold.cpp`(新增常量折叠)
- `src/ir/passes/ConstProp.cpp`(新增常量传播与条件分支化简)
- `src/ir/passes/CSE.cpp`(新增公共子表达式删除)
- `src/ir/passes/DCE.cpp`(新增死代码删除)
- `src/ir/passes/CFGSimplify.cpp`(新增控制流图简化)
- `src/mir/Lowering.cpp`(扩展 Phi 节点降低、修复指针类型加载、解决参数 GEP 错误、处理 Phi 栈槽分配)
- `src/main.cpp`(在编译器入口接入 IR 优化驱动程序)
- 新增本文档 `doc/Lab4-实验记录.md`
---
## 3. 关键困难与解决办法
### 3.1 困难一:指针大小截断(导致局部指针加载失效与段错误)
#### 现象
在将 IR 提升为 SSA 后,进行 GEP 和 Load/Store 寻址时,由于后端在处理指针类型(`PtrInt32``PtrFloat`)的变量加载时,原先只判断了是否为 float其余默认视作 32 位整型(使用 `W8` 寄存器加载)。这导致 64 位的指针值被截断为 32 位(高位信息丢失),寻址非法空间产生段错误。
#### 解决办法
我们在 `Lowering.cpp` 中修正了 Load 和 Store 指令的寄存器选择逻辑:当加载或写入的值是 `IsPtrInt32()``IsPtrFloat()` 时,强制选择 64 位的物理寄存器 `X8`(而非 32 位的 `W8`)。这样彻底保留了高位地址,防止了指针大小截断。
### 3.2 困难二GEP 中参数指针被当作本地数组处理
#### 现象
`15_graph_coloring.sy` 中,函数接收 `int color[]` 数组作为参数,然后在函数体里使用 `color[i]`。在 IR 中这是一个对参数指针的 GEP 操作。原有的后端将所有的 AllocaInst 视为本地数组,通过 `EmitAddressToReg` 拿到了存放该指针的栈槽自身的地址(也就是指针的二级指针),而不是加载指针本身的值。
#### 解决办法
`Lowering.cpp``case ir::Opcode::GEP` 中,对 AllocaInst 进行更精细的类型判别:
- 若 AllocaInst 的类型是数组类型(`IsArray()`),表示为本地数组,此时继续使用 `EmitAddressToReg` 获得基地址。
- 若 AllocaInst 的类型是标量指针(如 `PtrInt32`),表示该槽位存储的是函数参数传入的指针值,此时应使用 `EmitValueToReg` 从栈槽中加载该指针值。
这一改动使得跨函数指针传递和 GEP 访存 100% 准确。
### 3.3 困难三分支简化ConstProp导致的 Phi 节点不一致
#### 现象
在回归测试 `95_float.sy``if (0 || 0.3) ok();` 语句中IR 在逻辑 OR 展宽时产生了一个 Phi 节点汇合前驱的值。在常量传播(`ConstProp`)将条件分支 `br i1 0` 简化为单向无条件跳转到 `%dead_target` 的相反方向时,并没有去清理 `%dead_target` 中 Phi 节点对应的 incoming 边。
这就导致 Phi 节点残留了已删除前驱的脏数据,在后续 CFG 简化合并基本块时误将残留的 `0` 当成了唯一的 incoming 值进行替换,导致逻辑 `OR` 运算结果错误,少打印了一个 `ok`
#### 解决办法
`ConstProp.cpp` 简化条件分支时,识别出被裁剪掉的死前驱基本块 `dead_target`。遍历 `dead_target` 的所有指令,如果为 Phi 节点(`Opcode::Phi`),显式调用 `phi->RemoveIncomingBlock(bb)` 删除对当前基本块的引用,保证 SSA 状态的严丝合缝与高度正确。
### 3.4 困难四:参数分配的 4 字节栈槽溢出崩溃
#### 现象
在 AArch64 中,指针是 64 位的。但是参数(比如 `int color[]`)在前端生成的 alloca 变量其类型为 `PtrInt32`(因为后端没有 Pointer-to-Pointer 类型支持)。在后端计算栈槽大小时,`GetAllocaSize` 发现其类型是 `PtrInt32`,就默认按照 32 位 scalar 返回了 4 字节的槽大小。
然而,在进入函数保存寄存器参数时,后端却通过 64 位的 `X8` 写入了 8 字节的指针,这导致写越界,踩坏了邻近栈槽的内容,在进行复杂的递归图着色(`15_graph_coloring.sy`)时导致了野指针解引用和段错误。
#### 解决办法
`Lowering.cpp``GetAllocaSize` 中加入静态数据流依赖扫描:如果当前 AllocaInst 具有 `PtrInt32``PtrFloat` 类型,我们静态遍历其所在函数的全部 Store 指令。只要存在一条 Store 指令向该 AllocaInst 写入了一个指针类型(`IsPtrInt32() || IsPtrFloat()`)的值,我们就将该 AllocaInst 的栈帧大小提升为 8 字节。这完美解决了 64 位指针参数在 32 位 alloca 变量中的安全对齐。
---
## 4. 优化 Pass 实现细节
### 4.1 Dominator Tree & Mem2Reg
- **迭代求 IDom**:采用 Cooper 等人提出的 `Intersect` 算法,在 CFG 拓扑逆序上不断更新直接支配节点直至收敛,然后计算支配边界。
- **插 Phi 节点**:根据变量在哪些块被定义,将其支配边界块加入插 Phi 队列,并使用 `std::unordered_set` 去重。
- **变量重命名**:利用 DFS 支配树,使用栈维护当前活跃的 SSA 变量版本。在离开子树时回滚栈,并自动填充后继块中 Phi 节点的对应操作数。
### 2.2 Constant Folding & Propagation
- 能够静态计算 `ZExt`, `SIToFP`, `FPToSI` 等类型转换常量。
- 支持整型和浮点的双目运算折叠,以及比较操作折叠。
- 能够自动简化条件分支:当 `br i1` 的条件被证明为常数 `0``1` 时,直接替换为无条件分支 `br`
### 2.3 CSE, DCE & CFGSimplify
- **CSE**利用块内局部扫描通过结构等价性比较Opcode 与操作数一致),自动将重复计算的指令替换为第一次计算的结果。
- **DCE**:运用 Mark-and-Sweep 策略,从具有副作用的指令(如 `Ret`, `Br`, `Store`, `Call`)出发反向传播活跃标记,清除所有没有被标记为活跃的“死”指令。
- **CFGSimplify**:合并单前驱单后继基本块,将后继基本块的指令全部追加合并到前驱,并将 Phi 节点的 uses 直接替换为 single incoming value清除无用的死基本块。
---
## 5. 验证结果
我们对 `test/test_case/functional` 目录下的所有用例执行了 **开启优化** 的汇编与执行回归。所有用例均成功生成了 SSA 优化后的 IR 汇编并链接运行库,各项输出结果与退出码与预期文件(`.out`**100% 吻合,完全通过**
```bash
=== test/test_case/functional/05_arr_defn4.sy ===
退出码: 21
输出匹配: test/test_case/functional/05_arr_defn4.out
=== test/test_case/functional/09_func_defn.sy ===
退出码: 9
输出匹配: test/test_case/functional/09_func_defn.out
=== test/test_case/functional/11_add2.sy ===
退出码: 9
输出匹配: test/test_case/functional/11_add2.out
=== test/test_case/functional/13_sub2.sy ===
退出码: 248
输出匹配: test/test_case/functional/13_sub2.out
=== test/test_case/functional/15_graph_coloring.sy ===
1 2 3 2
退出码: 0
输出匹配: test/test_case/functional/15_graph_coloring.out
=== test/test_case/functional/22_matrix_multiply.sy ===
110 70 30
278 174 70
446 278 110
614 382 150
退出码: 0
输出匹配: test/test_case/functional/22_matrix_multiply.out
=== test/test_case/functional/25_scope3.sy ===
a
退出码: 46
输出匹配: test/test_case/functional/25_scope3.out
=== test/test_case/functional/29_break.sy ===
退出码: 201
输出匹配: test/test_case/functional/29_break.out
=== test/test_case/functional/36_op_priority2.sy ===
退出码: 24
输出匹配: test/test_case/functional/36_op_priority2.out
=== test/test_case/functional/95_float.sy ===
ok
... (全部ok)
退出码: 0
输出匹配: test/test_case/functional/95_float.out
=== test/test_case/functional/simple_add.sy ===
退出码: 3
输出匹配: test/test_case/functional/simple_add.out
```
## 6. 结论
本次 Lab4 构建了编译器中最重要的 SSA 中端优化核心。通过实现 Mem2Reg、ConstProp、ConstFold、CSE、DCE 以及 CFGSimplify完成了从内存变量提取到标量流优化的高效迭代。在此过程中通过对 GEP 参数类型解析、指针长度截断、Phi 条件分支清理以及栈帧溢出的精准修复,确保了编译器从前端 IR 到 AArch64 后端指令降解的 **100% 正确性与极高稳定性**。这也为后续 Lab5寄存器分配的完美开展做好了充足的铺垫。