|
|
|
|
@ -7,20 +7,22 @@ Lab5 的目标是让 IR 从“能跑”变成“跑得更好”。
|
|
|
|
|
|
|
|
|
|
在此基础上,可以逐步补上常量相关优化、无用代码删除、CFG 简化、公共子表达式消除等基础标量优化;如果你的实现方案里还需要其他局部优化,也可以按需继续扩展。
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
## 2. Lab5 要求
|
|
|
|
|
|
|
|
|
|
## 2. Mem2Reg
|
|
|
|
|
需要同学完成的事情并不复杂:先理解当前 IR/CFG 结构,明确“有用代码、无用代码、不可达代码”的区别;然后实现能够运行的基础标量优化,并把这些优化接入 `PassManager`,形成可重复执行的流程;最后通过测试确认优化前后语义一致。
|
|
|
|
|
|
|
|
|
|
## 3. Mem2Reg
|
|
|
|
|
|
|
|
|
|
在很多编译器中,AST lower 到 IR 时,局部变量通常先以“内存形式”表示,也就是先用 `alloca` 在栈上分配局部变量,再通过 `store/load` 完成写入和读取。
|
|
|
|
|
这种表示语义正确、实现直接,但会引入大量冗余内存访问,不利于常量传播、DCE、CSE 等标量优化。
|
|
|
|
|
|
|
|
|
|
`mem2reg`(memory to register)的目标,就是把这类 `alloca/load/store` 形式提升到 SSA 形式,让值尽量直接在 SSA Value 上传递。
|
|
|
|
|
|
|
|
|
|
### 2.1 Mem2Reg 的核心过程
|
|
|
|
|
### 3.1 Mem2Reg 的核心过程
|
|
|
|
|
|
|
|
|
|
典型流程通常包括几步:先识别可提升变量,找出由 `alloca` 分配且只通过 `load/store` 访问的局部变量;再构建 CFG,明确基本块与前驱/后继关系,为后续插入 `phi` 和重命名提供基础;接着在控制流汇合点插入 `phi`,并沿支配树完成变量重命名,为每次定义分配 SSA 版本;最后删除已经被提升掉的冗余 `alloca/load/store`。
|
|
|
|
|
|
|
|
|
|
### 2.2 Mem2Reg 的关键算法基础
|
|
|
|
|
### 3.2 Mem2Reg 的关键算法基础
|
|
|
|
|
|
|
|
|
|
支配树(Dominator Tree)用于描述“定义能影响到哪里”。若从入口到块 A 的所有路径都经过块 B,则 B 支配 A;变量重命名通常就建立在这层关系上,常见实现可采用 Lengauer-Tarjan 算法。
|
|
|
|
|
|
|
|
|
|
@ -29,10 +31,7 @@ Lab5 的目标是让 IR 从“能跑”变成“跑得更好”。
|
|
|
|
|
如果从更高层去看,Mem2Reg 本质上就是 SSA 构造流程在“可提升局部变量”上的工程化实现。典型路线仍然是:计算支配树,计算支配边界,插入 `phi`,再完成变量重命名。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 3. IR 的 use-def 关系
|
|
|
|
|
|
|
|
|
|
## 4. IR 的 use-def 关系
|
|
|
|
|
|
|
|
|
|
LLVM 中通常维护完整 `Use-User` 双向关系;当前仓库是最小 IR,实现较轻量。
|
|
|
|
|
|
|
|
|
|
@ -42,21 +41,12 @@ use-def(或 def-use)描述的是“值在哪里被定义、又在哪里被
|
|
|
|
|
|
|
|
|
|
在 IR 中维护好这层关系后,优化遍就能更快回答“这个值还有人用吗”“我要把旧值替换成新值,需要改哪些地方”这类问题。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### use-def 的作用
|
|
|
|
|
|
|
|
|
|
在优化阶段,use-def 关系的价值主要体现在几个方面:判断一个值是否还被使用会更直接,DCE 不必反复做全函数扫描;常量折叠、常量传播、复制传播这类局部重写也更容易精准找到所有使用点;同时,它还能降低很多优化遍的实现复杂度,并为后续扩展代数化简、CSE、部分冗余消除等优化打基础。
|
|
|
|
|
|
|
|
|
|
因此,把这层关系维护稳定,通常会明显降低 DCE、常量传播等优化的实现难度,也更利于后续扩展。
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 4. Lab5 要求
|
|
|
|
|
|
|
|
|
|
需要同学完成的事情并不复杂:先理解当前 IR/CFG 结构,明确“有用代码、无用代码、不可达代码”的区别;然后实现能够运行的基础标量优化,并把这些优化接入 `PassManager`,形成可重复执行的流程;最后通过测试确认优化前后语义一致。
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 5. 相关文件
|
|
|
|
|
|
|
|
|
|
以下文件与本实验内容相关,建议优先阅读。
|
|
|
|
|
@ -67,8 +57,6 @@ use-def(或 def-use)描述的是“值在哪里被定义、又在哪里被
|
|
|
|
|
- `src/ir/passes/DCE.cpp`
|
|
|
|
|
- `src/ir/passes/PassManager.cpp`
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 6. 算法说明
|
|
|
|
|
|
|
|
|
|
### 6.1 Dead(无用代码删除)
|
|
|
|
|
@ -91,8 +79,6 @@ use-def(或 def-use)描述的是“值在哪里被定义、又在哪里被
|
|
|
|
|
|
|
|
|
|
如果同一个表达式在程序中被多次计算,并且其操作数在计算之间没有改变,那么就可以只计算一次并复用结果。这类优化的直接收益,是减少重复计算、压缩指令数量、提升执行效率。实现时,通常会在基本块或更大范围内记录已经出现过的表达式;当再次遇到相同表达式且操作数未变化时,直接复用之前的结果,而不是重新生成同一计算。
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 7. 构建与验证
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
@ -117,5 +103,3 @@ cmake --build build -j "$(nproc)"
|
|
|
|
|
|
|
|
|
|
目标:脚本自动读取同名 `.in`,并将程序输出与退出码和同名 `.out` 比对,确保优化后程序行为与优化前保持一致。
|
|
|
|
|
完成 Lab5 后,应对 `test/test_case` 下全部测试用例逐个回归;如有需要,也可以自行编写批量测试脚本统一执行。
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|