Lab4: Implement basic scalar optimizations and lower Phi nodes to assembly
This commit is contained in:
150
doc/Lab4-实验记录.md
Normal file
150
doc/Lab4-实验记录.md
Normal file
@@ -0,0 +1,150 @@
|
||||
# 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(寄存器分配)的完美开展做好了充足的铺垫。
|
||||
Reference in New Issue
Block a user