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