Files
nudt-compiler-cpp/doc/Lab5-基本标量优化.md
2026-03-12 15:56:41 +08:00

220 lines
6.9 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.
# Lab5基本标量优化
## 1. 本实验定位
Lab5 的目标是让 IR 从“能跑”变成“跑的更好”。
在进入本实验的标量优化前,先完成或接入 `mem2reg`,将局部变量的 `alloca/load/store` 提升到 SSA 形式。
在当前编译器基础上,做基础标量优化,框架中给几种,可以按需补充:
1. 常量相关优化(常量折叠/传播)
2. 无用代码删除DCE
3. CFG 简化与不可达代码删除
4. 公共子表达式消除CSE
...
---
## 2. Mem2Reg
在很多编译器中AST lower 到 IR 时,局部变量通常先以“内存形式”表示:
1.`alloca` 在栈上分配局部变量
2.`store` 写变量
3.`load` 读变量
这种表示语义正确、实现直接但会引入大量冗余内存访问不利于常量传播、DCE、CSE 等标量优化。
`mem2reg`memory to register的目标就是把这类 `alloca/load/store` 形式提升到 SSA 形式,让值尽量直接在 SSA Value 上传递。
### 2.1 Mem2Reg 的核心过程
1. 识别可提升变量
找出由 `alloca` 分配、且只通过 `load/store` 访问的局部变量。
2. 构建 CFG
明确基本块与前驱/后继关系,为后续插入 `phi` 和重命名提供基础。
3. 插入 `phi`
在控制流汇合点合并来自不同路径的定义。
4. 变量重命名
沿支配树遍历,为每次定义分配 SSA 版本,保证“单次赋值”。
5. 删除冗余内存操作
提升完成后,移除对应的 `alloca/load/store`
### 2.2 Mem2Reg 的关键算法基础
1. 支配树Dominator Tree
若从入口到块 A 的所有路径都经过块 B则 B 支配 A。
支配树用于描述“定义能影响到哪里”,是变量重命名的基础。常见实现可采用 Lengauer-Tarjan 算法。
2. 支配边界Dominance Frontier
支配边界描述“支配关系结束并发生控制流汇合”的位置。
在 Mem2Reg 中,它的核心作用是确定 `phi` 函数插入点。
3. SSA 构造Cytron 框架)
典型流程为:计算支配树 -> 计算支配边界 -> 插入 `phi` -> 重命名变量。
Mem2Reg 本质上就是该 SSA 构造流程在“可提升局部变量”上的工程化实现。
---
## 3. IR 的 use-def 关系
LLVM 中通常维护完整 `Use-User` 双向关系;当前仓库是最小 IR实现较轻量。
### 什么是 use-def
use-def或 def-use描述的是“值在哪里被定义、又在哪里被使用”的关系
1. `def`:某条指令产生了一个值(定义点)。
2. `use`:其他指令把这个值当作操作数使用(使用点)。
在 IR 中维护好这层关系后,优化遍就能快速回答:
“这个值还有人用吗?”、“我要把旧值替换成新值,需要改哪些地方?”
### use-def 的作用
在优化阶段use-def 关系的价值主要体现在:
1. 判断“是否还被使用”更直接
DCE 可以直接依据某个值是否还有用户来决定是否可删,而不必每次全函数扫描。
2. 支持局部重写与传播
常量折叠、常量传播、复制传播时,需要把“旧值的所有使用点”替换为“新值”;有 use-def 后可以精准定位使用点。
3. 降低优化遍实现复杂度
没有 use-def 时,很多优化都要反复做全局查找;有 use-def 后可把复杂度和代码量都压下来。
4. 便于后续扩展更多优化
例如代数化简、CSE、部分冗余消除等都依赖稳定的 def-use/use-def 信息。
这会明显降低 DCE、常量传播等优化的实现复杂度也更利于后续扩展。
---
## 4. Lab5 要求
需要同学完成:
1. 理解当前 IR/CFG 结构,明确“有用代码、无用代码、不可达代码”的定义。
2. 完成可运行标量优化代码。
3. 将优化串联到 `PassManager`,形成可重复执行的优化流程。
4. 保证优化前后语义一致。
---
## 5. 当前代码框架(与 Lab5 相关)
1. IR 核心
- `include/ir/IR.h`
- `src/ir/Instruction.cpp`
- `src/ir/BasicBlock.cpp`
- `src/ir/Function.cpp`
- `src/ir/Module.cpp`
- `src/ir/IRPrinter.cpp`
2. 分析与优化
- `src/ir/analysis/DominatorTree.cpp`
- `src/ir/analysis/LoopInfo.cpp`
- `src/ir/passes/Mem2Reg.cpp`
- `src/ir/passes/ConstFold.cpp`
- `src/ir/passes/CSE.cpp`
- `src/ir/passes/DCE.cpp`
- `src/ir/passes/CFGSimplify.cpp`
- `src/ir/passes/PassManager.cpp`
3. 入口
- `src/main.cpp`
---
## 6. 需要修改的文件
1. 核心优化实现
- `src/ir/passes/Mem2Reg.cpp`(建议先完成,作为后续标量优化前置)
- `src/ir/passes/ConstFold.cpp`
- `src/ir/passes/CSE.cpp`
- `src/ir/passes/DCE.cpp`
- `src/ir/passes/CFGSimplify.cpp`
- `src/ir/passes/PassManager.cpp`
2. 视实现需要可能修改
- `include/ir/IR.h``src/ir/Instruction.cpp`(补充副作用/可删除性信息)
- `src/ir/IRPrinter.cpp`(调试输出增强)
- `src/ir/analysis/DominatorTree.cpp``src/ir/analysis/LoopInfo.cpp`(辅助分析)
- `src/ir/Value.cpp`(若补充 use-def 关系)
---
## 7. 算法说明
### 7.1 Dead无用代码删除
可以采用“标记 + 清扫”思路:
1. 从关键操作出发标记“有用”指令
2. 沿数据依赖和必要控制依赖扩展标记
3. 删除未标记指令
> 本实验不限定具体思路,实现可自由设计。
### 7.2 Clean
在 DCE 后对 CFG 做结构化清理,常见包括:
1. 冗余分支改写
2. 空块删除/绕过
3. 线性可合并块合并
4. 不可达块删除
### 7.3 优化顺序建议
建议仅约束一条:
1. `Mem2Reg` 在前面先执行一遍(将 IR 提升到更适合做标量优化的形式)。
其余优化遍(如 `ConstFold``CSE``DCE``CFGSimplify`)的组织顺序不做硬性规定,可根据你的实现自由设计;必要时可采用迭代方式直到 IR 不再变化。
### 7.4 公共子表达式消除Common Subexpression Elimination
原理:
如果同一个表达式在程序中被多次计算,并且其操作数在计算之间没有改变,则可以只计算一次,并复用计算结果。
作用:
避免重复计算,减少指令数量,提高执行效率。
实现思路:
在基本块或更大范围内记录已经计算过的表达式。再次遇到相同表达式且操作数未变化时,直接复用之前的结果,而不是重新生成同一计算。
---
## 8. 构建与验证
```bash
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j "$(nproc)"
```
### 8.1 观察 IR
```bash
./build/bin/compiler --emit-ir test/test_case/simple_add.sy
```
### 8.2 语义回归
```bash
./scripts/verify_ir.sh test/test_case/simple_add.sy test/test_result/ir --run
./scripts/verify_asm.sh test/test_case/simple_add.sy test/test_result/asm --run
```
目标:脚本自动读取同名 `.in`,并将程序输出与退出码和同名 `.out` 比对,确保优化后程序行为与优化前保持一致。
---