|
|
|
|
@ -0,0 +1,272 @@
|
|
|
|
|
# Lab4:基本标量优化
|
|
|
|
|
|
|
|
|
|
## 1. Lab4要求
|
|
|
|
|
|
|
|
|
|
Lab4要求在Lab2生成的中间表示(IR)基础上,实现一系列基本的标量优化遍(Pass)。优化的目标是减少指令数量、消除冗余计算、简化控制流,从而生成更高效的代码。实验需要学生自行设计和实现优化遍管理器(Pass Manager)以及优化遍和分析编。
|
|
|
|
|
|
|
|
|
|
## 2. 相关文件
|
|
|
|
|
|
|
|
|
|
以下文件与本实验内容相关,可以优先阅读:
|
|
|
|
|
|
|
|
|
|
- 定义的IR数据结构
|
|
|
|
|
```bash
|
|
|
|
|
./src/frontend/ir/context.rs # IR上下文,包含所有函数和全局变量
|
|
|
|
|
./src/frontend/ir/function.rs # 函数定义,包含基本块列表
|
|
|
|
|
./src/frontend/ir/basicblock.rs # 基本块,包含指令列表
|
|
|
|
|
./src/frontend/ir/instruction.rs # 指令定义及其操作数
|
|
|
|
|
./src/frontend/ir/global.rs # 全局变量定义
|
|
|
|
|
./src/frontend/ir/typ.rs # 类型系统
|
|
|
|
|
./src/frontend/ir/value.rs # SSA值定义
|
|
|
|
|
./src/frontend/ir/defuse.rs # 定义-使用链
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
- 优化遍管理器与优化遍(需自行创建各个文件)
|
|
|
|
|
```bash
|
|
|
|
|
./src/passes
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## 3. 实现说明
|
|
|
|
|
|
|
|
|
|
### 3.1 优化遍(Pass)的概念
|
|
|
|
|
|
|
|
|
|
在编译器设计中,**优化遍(Pass)** 是指对中间表示进行一次完整的遍历和变换,目的是改进代码质量。每个遍负责一种特定的优化任务,如删除未使用的代码、简化表达式等。
|
|
|
|
|
|
|
|
|
|
优化遍可以分为两类:
|
|
|
|
|
- **分析遍(Analysis Pass)**:收集程序信息但不修改IR,如构建支配树、计算活跃变量等
|
|
|
|
|
- **变换遍(Transform Pass)**:基于分析结果修改IR,如死代码消除、常量传播等
|
|
|
|
|
|
|
|
|
|
### 3.2 优化遍管理器(Pass Manager)
|
|
|
|
|
|
|
|
|
|
> 优化遍管理器负责协调各个优化遍的执行顺序和管理优化过程中的依赖关系。当有多个优化遍需要运行时,管理器需要决定:
|
|
|
|
|
> 1. **执行顺序**:某些优化可能依赖于其他优化先执行(如常量传播后可以进行死代码消除)
|
|
|
|
|
> 2. **迭代次数**:某些优化可能需要多次执行才能达到最佳效果(如简化控制流图后可能暴露新的死代码)
|
|
|
|
|
> 3. **分析结果复用**:多个优化遍可能共享相同的分析结果,避免重复计算
|
|
|
|
|
|
|
|
|
|
一个典型的遍管理器实现需要包含以下组件:
|
|
|
|
|
|
|
|
|
|
```rust
|
|
|
|
|
// pass_manager.rs 示例结构
|
|
|
|
|
pub struct PassManager {
|
|
|
|
|
passes: Vec<Box<dyn Pass>>,
|
|
|
|
|
analysis_manager: AnalysisManager,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub trait Pass {
|
|
|
|
|
fn name(&self) -> &str;
|
|
|
|
|
fn run_on_function(&mut self, func: &mut Function, ctx: &mut IRContext) -> bool;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub struct AnalysisManager {
|
|
|
|
|
domtrees: HashMap<FunctionId, DominatorTree>,
|
|
|
|
|
loops: HashMap<FunctionId, LoopInfo>,
|
|
|
|
|
// 其他分析结果
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 3.3 建议实现的优化遍
|
|
|
|
|
|
|
|
|
|
#### 3.3.1 死代码消除(Dead Code Elimination)
|
|
|
|
|
|
|
|
|
|
**死代码**是指计算结果从未被使用的指令,或者无法被执行到的代码。死代码消除可以删除这些无用指令,减少代码体积和执行时间。
|
|
|
|
|
|
|
|
|
|
**实现思路**:
|
|
|
|
|
1. 利用Lab2中实现的`defuse.rs`提供的定义-使用链信息
|
|
|
|
|
2. 从函数返回值、全局变量存储、函数调用等“有副作用”的指令开始,反向标记活跃指令
|
|
|
|
|
3. 未被标记的指令即为死代码,可以安全删除
|
|
|
|
|
|
|
|
|
|
**示例**:
|
|
|
|
|
```
|
|
|
|
|
%1 = add i32 %a, %b ; 结果未被使用,可删除
|
|
|
|
|
%2 = mul i32 %c, %d ; 结果被使用
|
|
|
|
|
%3 = add i32 %2, 1 ; 使用%2,保留
|
|
|
|
|
ret i32 %3
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
#### 3.3.2 常量传播与常量折叠(Constant Propagation & Folding)
|
|
|
|
|
|
|
|
|
|
**常量传播**是将变量的值替换为已知的常量,**常量折叠**是在编译时计算常量表达式的结果。
|
|
|
|
|
|
|
|
|
|
**实现思路**:
|
|
|
|
|
1. 遍历每条指令,维护每个SSA值的常量状态(常量/非常量/未定义)
|
|
|
|
|
2. 当一条指令的所有操作数都是常量时,计算结果并替换为常量值
|
|
|
|
|
3. 将常量值传播到后续使用该值的位置
|
|
|
|
|
|
|
|
|
|
**示例**:
|
|
|
|
|
```
|
|
|
|
|
%1 = add i32 2, 3 ; 折叠为 %1 = i32 5
|
|
|
|
|
%2 = mul i32 %1, 4 ; 传播后 %2 = mul i32 5, 4,再折叠为 i32 20
|
|
|
|
|
ret i32 %2 ; 最终为 ret i32 20
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
#### 3.3.3 控制流图简化(Control Flow Graph Simplification)
|
|
|
|
|
|
|
|
|
|
**控制流图简化**包括删除不可达基本块、合并空基本块、简化跳转链等。
|
|
|
|
|
|
|
|
|
|
**实现思路**:
|
|
|
|
|
1. **删除不可达基本块**:从入口基本块开始标记可达块,删除未被标记的块
|
|
|
|
|
2. **合并基本块**:如果一个基本块只有一个后继,且后继只有一个前驱,可以合并
|
|
|
|
|
3. **简化跳转**:将`br label %L1`且`L1`只有一个无条件跳转到`L2`的情况,直接跳转到`L2`
|
|
|
|
|
|
|
|
|
|
**示例**:
|
|
|
|
|
```
|
|
|
|
|
; 优化前
|
|
|
|
|
entry:
|
|
|
|
|
br label %bb1
|
|
|
|
|
bb1:
|
|
|
|
|
br label %bb2
|
|
|
|
|
bb2:
|
|
|
|
|
ret i32 0
|
|
|
|
|
|
|
|
|
|
; 优化后(合并bb1)
|
|
|
|
|
entry:
|
|
|
|
|
br label %bb2
|
|
|
|
|
bb2:
|
|
|
|
|
ret i32 0
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
#### 3.3.4 内存到寄存器的提升(Mem2Reg)
|
|
|
|
|
|
|
|
|
|
**Mem2Reg**是将栈上分配的内存变量提升为SSA值,这是实现后续优化的基础。SysY语言中的局部变量通常通过`alloca`分配,然后使用`load`/`store`访问,这种形式不利于优化。
|
|
|
|
|
|
|
|
|
|
**实现思路**:
|
|
|
|
|
1. 识别可以提升的`alloca`指令(未被取地址、无非法的指针运算)
|
|
|
|
|
2. 使用经典的**插入φ函数**和**变量重命名**算法
|
|
|
|
|
3. 将所有`load`替换为对应的SSA值,删除`alloca`和`store`指令
|
|
|
|
|
|
|
|
|
|
**示例**:
|
|
|
|
|
```
|
|
|
|
|
; 优化前
|
|
|
|
|
%x = alloca i32
|
|
|
|
|
store i32 10, i32* %x
|
|
|
|
|
%1 = load i32, i32* %x
|
|
|
|
|
%2 = add i32 %1, 1
|
|
|
|
|
ret i32 %2
|
|
|
|
|
|
|
|
|
|
; 优化后
|
|
|
|
|
%x.0 = i32 10
|
|
|
|
|
%2 = add i32 %x.0, 1
|
|
|
|
|
ret i32 %2
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
#### 3.3.5 全局值编号(Global Value Numbering)【选做】
|
|
|
|
|
|
|
|
|
|
**全局值编号**用于消除冗余计算——当两条指令计算相同的表达式时,可以复用第一次计算的结果。
|
|
|
|
|
|
|
|
|
|
**实现思路**:
|
|
|
|
|
1. 为每个表达式计算哈希值(考虑操作符和操作数)
|
|
|
|
|
2. 在整个函数范围内,如果遇到相同哈希的表达式,且操作数在当前点仍然可用,则替换为之前的结果
|
|
|
|
|
|
|
|
|
|
**示例**:
|
|
|
|
|
```
|
|
|
|
|
%1 = add i32 %a, %b
|
|
|
|
|
%2 = add i32 %a, %b ; 与%1相同,可替换为%1
|
|
|
|
|
%3 = mul i32 %1, %c
|
|
|
|
|
%4 = mul i32 %1, %c ; 与%3相同,可替换为%3
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 3.4 优化遍的组织顺序
|
|
|
|
|
|
|
|
|
|
建议的优化遍执行顺序(可以多次迭代):
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
第一轮(基本清理):
|
|
|
|
|
控制流图简化 → 死代码消除 → 常量传播/折叠
|
|
|
|
|
|
|
|
|
|
第二轮(核心优化):
|
|
|
|
|
内存到寄存器提升 → 控制流图简化 → 常量传播/折叠 → 死代码消除
|
|
|
|
|
|
|
|
|
|
第三轮(高级优化,可选):
|
|
|
|
|
全局值编号 → 常量传播 → 死代码消除 → 控制流图简化
|
|
|
|
|
|
|
|
|
|
最终清理:
|
|
|
|
|
死代码消除 → 控制流图简化
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 3.5 优化遍的接口设计
|
|
|
|
|
|
|
|
|
|
建议为每个优化遍设计统一的接口:
|
|
|
|
|
|
|
|
|
|
```rust
|
|
|
|
|
pub trait FunctionPass {
|
|
|
|
|
/// 在函数上运行优化遍
|
|
|
|
|
/// 返回值表示是否修改了IR
|
|
|
|
|
fn run_on_function(&mut self, func: &mut Function, ctx: &mut IRContext) -> bool;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub trait ModulePass {
|
|
|
|
|
/// 在整个模块上运行优化遍
|
|
|
|
|
fn run_on_module(&mut self, module: &mut IRContext) -> bool;
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## 4. 当前示例实现说明
|
|
|
|
|
|
|
|
|
|
**注意:** 本实验的`passes`文件夹初始为空,需要学生自行实现优化遍管理器和优化遍。示例代码中未提供任何优化遍的实现,这是有意为之,目的是让学生完整地经历优化遍的设计和实现过程。
|
|
|
|
|
|
|
|
|
|
学生需要完成的工作包括:
|
|
|
|
|
|
|
|
|
|
1. **设计优化遍的抽象接口**:定义`Pass` trait,统一优化遍的调用方式
|
|
|
|
|
|
|
|
|
|
2. **实现遍管理器**:
|
|
|
|
|
- 支持注册多个优化遍
|
|
|
|
|
- 管理优化遍的执行顺序
|
|
|
|
|
- 支持多次迭代优化
|
|
|
|
|
- 管理分析结果的复用
|
|
|
|
|
|
|
|
|
|
3. **实现优化遍**:
|
|
|
|
|
- 推荐:死代码消除、常量传播/折叠、控制流图简化、Mem2Reg、全局值编号等
|
|
|
|
|
|
|
|
|
|
4. **集成到编译器主流程**:
|
|
|
|
|
- 在`main.rs`中处理`-O1`命令行选项
|
|
|
|
|
- 在IR生成后、MIR生成前调用优化遍管理器
|
|
|
|
|
|
|
|
|
|
## 5. 构建与生成流程
|
|
|
|
|
|
|
|
|
|
运行实例代码,查看优化效果可以使用下面的命令,在项目根目录下:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
# 构建编译器
|
|
|
|
|
cargo build -r
|
|
|
|
|
|
|
|
|
|
# 生成优化后的汇编代码(启用-O1优化)
|
|
|
|
|
./target/release/compiler -O1 -asm ./test/output/simple_add.s ./test/test_case/functional/simple_add.sy
|
|
|
|
|
|
|
|
|
|
# 生成优化后的IR文件(便于调试)
|
|
|
|
|
./target/release/compiler -O1 -ir ./test/output/simple_add.ll ./test/test_case/functional/simple_add.sy
|
|
|
|
|
|
|
|
|
|
# 对比优化前后的汇编代码
|
|
|
|
|
# 先不优化生成一份
|
|
|
|
|
./target/release/compiler -asm ./test/output/simple_add_no_opt.s ./test/test_case/functional/simple_add.sy
|
|
|
|
|
# 再优化生成一份
|
|
|
|
|
./target/release/compiler -O1 -asm ./test/output/simple_add_opt.s ./test/test_case/functional/simple_add.sy
|
|
|
|
|
# 使用diff工具对比
|
|
|
|
|
diff ./test/output/simple_add_no_opt.s ./test/output/simple_add_opt.s
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 验证优化效果
|
|
|
|
|
|
|
|
|
|
可以通过以下方式验证优化是否正确且有效:
|
|
|
|
|
|
|
|
|
|
1. **正确性验证**:运行测试用例,确保优化后的程序行为不变
|
|
|
|
|
```bash
|
|
|
|
|
# 生成优化后的汇编
|
|
|
|
|
./target/release/compiler -O1 -asm ./test/output/test.s ./test/test_case/functional/test.sy
|
|
|
|
|
# 编译汇编为可执行文件(假设使用gcc)
|
|
|
|
|
gcc ./test/output/test.s -o ./test/output/test
|
|
|
|
|
# 运行并检查返回值
|
|
|
|
|
./test/output/test
|
|
|
|
|
echo $? # 应与预期一致
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
2. **性能验证**:统计生成的指令数量或者对比优化前后的运行时间,需要编写脚本,推荐使用Python
|
|
|
|
|
```bash
|
|
|
|
|
# 统计优化前后汇编指令行数(粗略估计)
|
|
|
|
|
wc -l ./test/output/simple_add_no_opt.s
|
|
|
|
|
wc -l ./test/output/simple_add_opt.s
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
3. **IR级别验证**:观察优化前后的IR变化
|
|
|
|
|
```bash
|
|
|
|
|
./target/release/compiler -O1 -ir ./test/output/simple_add_opt.ll ./test/test_case/functional/simple_add.sy
|
|
|
|
|
cat ./test/output/simple_add_opt.ll # 查看优化后的IR
|
|
|
|
|
```
|