安峻邑 2 weeks ago
parent 55d92cda42
commit 351c1128fc

@ -1,89 +0,0 @@
# Lab1语法树构建
## 1. 本实验定位
本仓库是一个“最小可运行编译器框架”,当前仅实现 SysY 的极小子集(示例级功能,主要用于演示完整构建流程)。
课程目标不是停留在这个最小子集,而是让同学们在该框架上逐步补全完整 SysY并最终完成完整编译器前端、中端、后端
## 2. Lab1 要求
Lab1 聚焦前端第一步:词法/语法分析。
需要同学完成:
1. 依据 SysY 规范扩展文法 `src/antlr4/SysY.g4`
2. 通过构建流程重新生成 Lexer/Parser。
3. 让更多合法 SysY 程序可以被解析通过(不再仅限当前最小样例)。
## 3. 相关文件
以下文件与本实验内容相关,建议优先阅读。
- `src/antlr4/SysY.g4`
- `src/frontend/AntlrDriver.cpp`
- `src/frontend/SyntaxTreePrinter.cpp`
## 4. 当前示例实现说明
当前仓库仅实现最小子集:
1. 主要覆盖 `int main() { ... }` 这一固定函数形态。
2. 只包含少量声明/返回/表达式能力;当前默认示例主要覆盖简单加法。
3. 示例用例位于 `test/test_case/functional/simple_add.sy`
## 5. 构建与生成流程
Lab1 中需要先生成 Lexer/Parser 相关文件,再执行 CMake 构建。
为了只聚焦语法树构建,建议启用 `parse-only` 模式,仅编译前端解析与语法树打印,不编译 `sem` / `irgen` / `mir`
Lexer/Parser 生成文件统一位于:
- `build/generated/antlr4/`
如需手动生成 Lexer/Parser
```bash
mkdir -p build/generated/antlr4
java -jar third_party/antlr-4.13.2-complete.jar \
-Dlanguage=Cpp \
-visitor -no-listener \
-Xexact-output-dir \
-o build/generated/antlr4 \
src/antlr4/SysY.g4
```
随后执行 Lab1 构建,下面的命令是只编译运行前端:
```bash
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DCOMPILER_PARSE_ONLY=ON
cmake --build build -j "$(nproc)"
```
如果后续需要继续验证 `sem` / `irgen` / `mir`,再使用全量构建。需要注意的是,由于 `irgen` 需要直接遍历 ANTLR 生成的语法树,`sem` / `irgen` 会直接依赖 `SysYParser::*Context` 的节点类型、层级结构和访问接口。因此当 `src/antlr4/SysY.g4` 被扩展后,如果后续阶段代码没有同步适配新的语法树结构,就可能在全量构建或运行时出现报错;这部分适配工作属于 Lab2 及后续实验需要继续完成的内容。
全量构建命令
```bash
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j "$(nproc)"
```
## 6. Lab1 测试建议
先用单个样例检查语法树输出是否基本正常:
1. 运行 `./build/bin/compiler --emit-parse-tree <case.sy>` 检查解析是否成功。
2. 出现报错时优先回查 `SysY.g4` 逻辑。
```bash
# 仅输出语法树
./build/bin/compiler --emit-parse-tree test/test_case/functional/simple_add.sy
```
但最终不能只检查 `simple_add`。完成 Lab1 后,应至少对 `test/test_case` 下全部 `.sy` 用例逐个验证解析是否成功;如有需要,也可以自行编写批量测试脚本统一执行。
## 7. 关于 AST 的建议
同学们也可以自行设计一层抽象语法树AST将 ANTLR 语法树先转换为 AST再进入后续阶段。
这样可以减少对具体文法细节的依赖使语义分析、IR 生成和后续扩展更清晰。这里不做具体要求。

@ -1,109 +0,0 @@
# Lab2中间表示生成
## 1. 本实验定位
Lab2 的目标是在该示例基础上扩展语义覆盖范围,并逐步把更多 SysY 语法正确翻译为 IR。
## 2. Lab2 要求
需要同学完成:
1. 熟悉 IR 相关数据结构与构建接口。
2. 理解当前语法树 -> 语义检查 -> IR 的最小实现流程。
3. 在现有框架上补充语义检查与 IR 生成功能,使其覆盖课程要求的 SysY 语法。
## 3. 相关文件
以下文件与本实验内容相关,建议优先阅读。
- `include/sem/Sema.h`
- `include/sem/SymbolTable.h`
- `src/sem/Sema.cpp`
- `src/sem/SymbolTable.cpp`
- `include/ir/IR.h`
- `src/ir/Context.cpp`
- `src/ir/Value.cpp`
- `src/ir/Instruction.cpp`
- `src/ir/BasicBlock.cpp`
- `src/ir/Function.cpp`
- `src/ir/Module.cpp`
- `src/ir/IRBuilder.cpp`
- `src/ir/IRPrinter.cpp`
- `include/irgen/IRGen.h`
- `src/irgen/IRGenDecl.cpp`
- `src/irgen/IRGenStmt.cpp`
- `src/irgen/IRGenExp.cpp`
- `src/irgen/IRGenFunc.cpp`
- `src/irgen/IRGenDriver.cpp`
## 4. 当前最小示例实现说明
当前语法树 -> 语义检查 -> IR 仅覆盖最小子集:
1. 常量整数、变量引用、二元加法表达式。
2. 局部变量声明(当前采用 LLVM 前端常见的 `alloca/load/store` 内存模型)。
3. `return` 语句。
4. 单函数 `main` 的最小流程。
其中,`sema` 负责最基本的名称绑定与合法性检查,`irgen` 在此基础上继续生成 IR。
如果语义检查阶段没有补全,后续 IR 生成阶段通常也无法正确处理变量引用、声明绑定等逻辑。
当前 `irgen` 的组织方式基于 ANTLR Visitor 的实现。
`IRGenImpl` 继承自 `SysYBaseVisitor`,按照语法树节点类型分发到不同的 `visit*` 函数中完成 IR 生成。整体流程大致是:
1. `GenerateIR(tree, sema)` 先创建 `Module`,再构造 `IRGenImpl`
2. 从语法树根节点开始访问,进入 `visitCompUnit`
3. `visitFuncDef``Module` 中创建 `Function`,并把 `IRBuilder` 的插入点设置到入口基本块。
4. `visitBlockStmt` / `visitBlockItem` 顺序遍历块内声明与语句。
5. `visitDecl` / `visitVarDef` 为局部变量生成 `alloca` 和初始化 `store`
6. `visitExp` 相关函数递归生成常量、`load`、`add` 等表达式值。
7. `visitReturnStmt` 生成 `ret`,终结当前基本块。
需要强调的是:当前 `IRGen` 还只是一个教学用的最小实现。它只支持 `int main()`、局部 `int` 变量、整数字面量、变量读取、二元加法与 `return`;函数形参、全局变量、控制流、调用、数组等都还需要同学后续补充。
说明:当前阶段变量统一采用内存模型:先 `alloca` 分配栈槽,再通过 `store/load` 读写。即使变量由常量初始化(如 `int a = 1;`),也会先 `store` 到栈槽,而不是直接把变量替换成 SSA 值。后续实验中,同学可按需求再重构。
此外,当前 IR 还维护了最基本的 use-def 关系:每个 `Value` 会记录自己的 `Use`/`User` 信息,`Instruction` 通过 operand 列表与这些关系自动关联。
这对后续做数据流分析、死代码删除、常量传播等优化会很有帮助;但目前相关实现,接口仍不完整,后续实验中还需要同学继续补充和完善。
## 5. 语法树与 Sema / IRGen 的关系
当前项目中的 `sema``irgen` 都不是面向独立 AST 设计的,而是直接基于 ANTLR 生成的语法树节点,并通过 Visitor 方式完成语义检查与 IR 生成。
因此,`SysY.g4` 中 rule 的命名、层级结构以及 labeled alternative 的写法,会直接影响 `SysYParser::*Context` 的类型名和访问接口;一旦 grammar 发生变化,`sem` / `irgen` 中对应的 `visit*` 逻辑通常也需要同步修改。
如果 grammar 扩展后 `sem` / `irgen` 没有同步修改,常见现象包括:
1. 编译阶段报错,例如某个 `SysYParser::*Context` 类型不存在,或某个成员函数不存在。
2. 运行阶段报错,例如进入 `暂不支持的表达式形式`、`暂不支持的语句类型`,或名称绑定失败等分支。
遇到这类问题时,需要同学对照 `SysY.g4`、ANTLR 生成的 `SysYParser.h`,以及 `src/sem` / `src/irgen` 中的 Visitor 遍历逻辑,完成对应的接口调整与功能补全。
## 6. 构建与运行
```bash
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j "$(nproc)"
```
## 7. Lab2 验证方式
可先用单个样例检查 IR 输出是否基本正确:
```bash
./build/bin/compiler --emit-ir test/test_case/functional/simple_add.sy
```
推荐使用统一脚本验证 “IR -> LLVM 后端 -> 可执行程序” 整体链路。`--run` 模式下会自动读取同名 `.in`,并将程序输出与退出码和同名 `.out` 比对,用于验证 IR 的正确性:
```bash
./scripts/verify_ir.sh test/test_case/functional/simple_add.sy test/test_result/function/ir --run
```
但最终不能只检查 `simple_add`。完成 Lab2 后,应对 `test/test_case` 下全部测试用例逐个回归,确认 IR 生成与 `--run` 链路都能通过;如有需要,也可以自行编写批量测试脚本统一执行。

@ -1,60 +0,0 @@
# Lab3指令选择与汇编生成
## 1. 本实验定位
本仓库当前提供了一个“最小可运行”的 IR -> AArch64 汇编示例链路。
Lab3 的目标是在该示例基础上扩展后端语义覆盖范围,逐步把更多 SysY IR 正确翻译为目标平台汇编代码。
## 2. Lab3 要求
需要同学完成:
1. 熟悉 MIR 相关数据结构与后端阶段接口。
2. 理解当前 IR -> MIR -> 汇编输出的最小实现流程。
3. 在现有框架上扩展后端代码生成能力,使其覆盖课程要求的 SysY 语义。
## 3. 相关文件
以下文件与本实验内容相关,建议优先阅读。
- `include/mir/MIR.h`
- `src/mir/Lowering.cpp`
- `src/mir/RegAlloc.cpp`
- `src/mir/FrameLowering.cpp`
- `src/mir/AsmPrinter.cpp`
## 4. 当前最小示例实现说明
当前 IR -> 汇编仅覆盖最小子集:
1. 仅支持单函数 `main`、单基本块的最小流程。
2. 仅支持由当前 Lab2 最小 IR 产生的 `alloca`、`load`、`store`、`add`、`ret`。
3. 局部变量与中间结果当前统一采用栈槽模型:所有值先映射到栈槽,再通过固定寄存器 `w0`、`w8`、`w9` 配合 `ldur/stur/add` 生成汇编。
4. `RegAlloc` 当前仅执行最小一致性检查,不实现真实寄存器分配。
5. `FrameLowering` 当前会插入最小序言/尾声,并按 16 字节对齐栈帧。
说明:当前阶段后端主要用于演示完整流程。即使中间值可以暂存在寄存器中,也会先写回栈槽,而不是直接构造更接近最终机器代码的寄存器流。后续实验中,同学可按需求继续扩展指令选择、寄存器分配、调用约定与控制流相关功能。
## 5. 构建与运行
```bash
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j "$(nproc)"
```
## 6. Lab3 验证方式
可先用单个样例检查汇编输出是否基本正确:
```bash
./build/bin/compiler --emit-asm test/test_case/functional/simple_add.sy
```
推荐使用统一脚本验证 “源码 -> 汇编 -> 可执行程序” 整体链路。`--run` 模式下会自动读取同名 `.in`,并将程序输出与退出码和同名 `.out` 比对,用于验证后端代码生成的正确性:
```bash
./scripts/verify_asm.sh test/test_case/functional/simple_add.sy test/test_result/function/asm --run
```
若最终输出 `输出匹配: test/test_case/simple_add.out`,说明当前示例用例 `return a + b` 的完整后端链路已经跑通。
但最终不能只检查 `simple_add`。完成 Lab3 后,应对 `test/test_case` 下全部测试用例逐个回归,确认代码生成结果能够通过统一验证;如有需要,也可以自行编写批量测试脚本统一执行。

@ -1,111 +0,0 @@
# Lab4基本标量优化
## 1. 本实验定位
为了提升最终生成汇编码的实际运行性能,本实验需要引入基础标量优化;这一部分优化通常能够带来较为明显的性能提升。
在进入本实验的标量优化前,先完成或接入 `mem2reg`,将局部变量的 `alloca/load/store` 提升到 SSA 形式。
在此基础上可以逐步补上常量相关优化、无用代码删除、CFG 简化、公共子表达式消除等基础标量优化;如果你的实现方案里还需要其他局部优化,也可以按需继续扩展。
## 2. Lab4 要求
需要同学完成的事情并不复杂:先理解当前 IR/CFG 结构,然后实现能够运行的基础标量优化,并把这些优化接入 `PassManager`,形成可重复执行的流程;最后通过测试确认优化前后语义一致。
## 3. 相关文件
以下文件与本实验内容相关,建议优先阅读。
- `include/ir/IR.h`
- `src/ir/passes/Mem2Reg.cpp`
- `src/ir/passes/ConstFold.cpp`
- `src/ir/passes/ConstProp.cpp`
- `src/ir/passes/DCE.cpp`
- `src/ir/passes/PassManager.cpp`
## 4. 当前基础与前置准备
### 4.1 Mem2Reg
在很多编译器中AST lower 到 IR 时,局部变量通常先以“内存形式”表示,也就是先用 `alloca` 在栈上分配局部变量,再通过 `store/load` 完成写入和读取。
这种表示语义正确、实现直接但会引入大量冗余内存访问不利于常量传播、DCE、CSE 等标量优化。
`mem2reg`memory to register的目标就是把这类 `alloca/load/store` 形式提升到 SSA 形式,让值尽量直接在 SSA Value 上传递。
#### 4.1.1 Mem2Reg 的核心过程
典型流程通常包括几步:先识别可提升变量,找出由 `alloca` 分配且只通过 `load/store` 访问的局部变量;再构建 CFG明确基本块与前驱/后继关系,为后续插入 `phi` 和重命名提供基础;接着在控制流汇合点插入 `phi`,并沿支配树完成变量重命名,为每次定义分配 SSA 版本;最后删除已经被提升掉的冗余 `alloca/load/store`
#### 4.1.2 Mem2Reg 的关键算法基础
支配树Dominator Tree用于描述“定义能影响到哪里”。若从入口到块 A 的所有路径都经过块 B则 B 支配 A变量重命名通常就建立在这层关系上常见实现可采用 Lengauer-Tarjan 等算法。
支配边界Dominance Frontier描述的是“支配关系结束并发生控制流汇合”的位置。在 Mem2Reg 中,它的核心作用是确定 `phi` 函数插入点。
如果从更高层去看Mem2Reg 本质上就是 SSA 构造流程在“可提升局部变量”上的工程化实现。典型路线仍然是:计算支配树,计算支配边界,插入 `phi`,再完成变量重命名。
### 4.2 IR 的 use-def 关系
LLVM 中通常维护完整 `Use-User` 双向关系;当前仓库是最小 IR实现较轻量。
#### 4.2.1 什么是 use-def
use-def或 def-use描述的是“值在哪里被定义、又在哪里被使用”的关系。`def` 指某条指令产生了一个值,`use` 指其他指令把这个值当作操作数使用。
在 IR 中维护好这层关系后,优化遍就能更快回答“这个值还有人用吗”“我要把旧值替换成新值,需要改哪些地方”这类问题。
#### 4.2.2 use-def 的作用
在优化阶段use-def 关系的价值主要体现在几个方面判断一个值是否还被使用会更直接DCE 不必反复做全函数扫描常量折叠、常量传播、复制传播这类局部重写也更容易精准找到所有使用点同时它还能降低很多优化遍的实现复杂度并为后续扩展代数化简、CSE、部分冗余消除等优化打基础。
因此,把这层关系维护稳定,通常会明显降低 DCE、常量传播等优化的实现难度也更利于后续扩展。
## 5. 可实现的优化方向与实现提示
### 5.1 Constant Folding / Constant Propagation
常量相关优化通常包括常量折叠Constant Folding与常量传播Constant Propagation。前者是指当一条指令的操作数已经都是常量时直接在编译期计算结果并用常量替换原指令后者是指当某个 SSA 值已知为常量时,将该常量继续传播到其使用点,从而为后续进一步折叠、删除冗余分支和清理死代码创造条件。
### 5.2 Dead Code Elimination (DCE)
可以采用“标记 + 清扫”思路:先从会影响程序可观察行为的指令出发,标记为“有用”指令,例如 ret、分支跳转、store 以及可能具有副作用的 call再沿这些指令的数据依赖反向传播将其依赖的定义一并标记为有用最后删除其余未被标记、且本身不具有副作用的指令。
> 本实验不限定具体思路,实现可自由设计。
### 5.3 CFG Simplification
在 DCE 之后,通常还需要对 CFG 做一轮结构化清理,例如改写冗余分支、删除或绕过空块、合并线性可拼接的基本块,以及清理不可达块。
### 5.4 公共子表达式消除Common Subexpression Elimination
如果同一个表达式在程序中被多次计算,并且其操作数在计算之间没有改变,那么就可以只计算一次并复用结果。这类优化的直接收益,是减少重复计算、压缩指令数量、提升执行效率。实现时,通常会在基本块或更大范围内记录已经出现过的表达式;当再次遇到相同表达式且操作数未变化时,直接复用之前的结果,而不是重新生成同一计算。
### 5.5 优化顺序建议
这里建议只固定一个基本约束:先执行一遍 `Mem2Reg`,把 IR 提升到更适合做标量优化的形式。
其余优化遍(如 `ConstFold`、`CSE`、`DCE`、`CFGSimplify`)的组织顺序不做硬性规定,可根据你的实现自由设计;可以采用优化遍多次迭代方式,直到 IR 不再变化。
## 6. 构建与验证
```bash
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j "$(nproc)"
```
### 6.1 观察 IR
```bash
./build/bin/compiler --emit-ir test/test_case/functional/simple_add.sy
```
这条命令只适合先观察单个样例的 IR 形态。完成 Lab4 后,不能只检查 `simple_add`,还应覆盖 `test/test_case` 下全部测试用例。
### 6.2 语义回归
```bash
./scripts/verify_ir.sh test/test_case/functional/simple_add.sy test/test_result/function/ir --run
./scripts/verify_asm.sh test/test_case/functional/simple_add.sy test/test_result/function/asm --run
```
目标:脚本自动读取同名 `.in`,并将程序输出与退出码和同名 `.out` 比对,确保优化后程序行为与优化前保持一致。
完成 Lab4 后,应对 `test/test_case` 下全部测试用例逐个回归;如有需要,也可以自行编写批量测试脚本统一执行。

@ -1,97 +0,0 @@
# Lab5寄存器分配与后端优化
## 1. 本实验定位
本仓库当前提供了一个“最小可运行”的 IR -> AArch64 汇编示例链路。
Lab5 的目标是在 Lab3 示例基础上,把“固定寄存器 + 栈槽”的最小后端实现推进为“虚拟寄存器 -> 物理寄存器”的真实后端阶段,并在此基础上补充局部后端优化,为完整 SysY 后端打基础。
## 2. Lab5 要求
需要同学完成:
1. 熟悉 MIR 中寄存器、操作数、栈槽与机器函数之间的关系,并理解当前 IR -> MIR -> 汇编输出流程中寄存器相关部分的最小实现现状。
2. 扩展当前 MIR 表达,使指令选择阶段能够产出虚拟寄存器,而不是继续固定使用 `w0`、`w8`、`w9`。
3. 在现有框架上实现真实寄存器分配,并处理 spill/reload、栈槽管理、callee-saved 保存恢复等后续问题。
4. 图着色寄存器分配与线性扫描寄存器分配均可作为实现路线,同学可自行选择其中一种完成;后端优化部分也不限定具体实现方式,只要求功能正确、收益明确。
5. 在寄存器分配结果基础上,补充后端局部优化流程,减少明显冗余机器指令与低效访存。可实现的优化包括但不限于:窥孔优化、冗余 `move/copy` 消除、局部访存冗余消除,以及简单恒等指令消除(如 `add/sub ..., #0`)。
6. 在 `test/test_case` 提供的全部测试用例上验证正确性,并在保证功能正确的前提下尽量减少冗余 spill/reload、无效拷贝、冗余访存与低效机器指令提升生成代码质量。
## 3. 相关文件
以下文件与本实验内容相关,建议优先阅读。
- `include/mir/MIR.h`
- `src/mir/Lowering.cpp`
- `src/mir/RegAlloc.cpp`
- `src/mir/FrameLowering.cpp`
- `src/mir/passes/Peephole.cpp`
## 4. 当前最小示例实现说明
当前后端中的寄存器分配与后端优化相关实现仍停留在最小示例阶段:
1. `Lowering.cpp` 当前直接使用固定物理寄存器 `w0`、`w8`、`w9` 生成机器指令,而不是先生成虚拟寄存器。
2. `RegAlloc.cpp` 当前仅执行最小一致性检查,不实现真实寄存器分配。
3. 当前 MIR 主要围绕单函数 `main`、单基本块与最小指令子集工作,尚未形成完整课程版本所需的寄存器分配基础设施。
4. `FrameLowering.cpp``AsmPrinter.cpp` 当前默认前面阶段已经给出可直接落地的固定寄存器结果,并未围绕完整 RA 流程展开。
5. `src/mir/passes/Peephole.cpp``src/mir/passes/PassManager.cpp` 当前仅保留了最小注释框架,尚未形成真实可运行的后端优化流程。
6. 因此,当前代码实际上**没有实现完整的寄存器分配与后端优化**,这一部分需要同学自行完成。
说明:本阶段不应继续沿用 Lab3 的“所有中间值统一写回栈槽 + 固定寄存器临时搬运”的做法,而应先把指令选择结果改造成带虚拟寄存器的 MIR再进入寄存器分配阶段在寄存器分配与栈帧落地完成后再针对最终机器指令序列做局部后端优化。无论选择哪一种寄存器分配算法都需要先解决几个共同前提为机器指令补充 `use/def` 信息、能够遍历机器基本块与控制流关系、为虚拟寄存器维护分配状态,并在 spill 后为新引入的访存指令重新参与后续流程。
后端优化部分建议保持“局部、可验证、与当前框架贴合”的范围,不必一开始就追求很重的优化框架。更合适的做法,是先围绕最终机器指令里最常见、最容易验证收益的冗余展开,例如删除明显多余的 `move/copy`,合并常见的短指令模式,清理无效恒等操作,以及减少相邻、无干扰的重复 `load/store`。如果寄存器分配或固定模板代码引入了比较机械的搬运和访存,也可以优先从这些最直观的低效模式入手做局部改进。
说明:本实验中的后端优化重点是“局部机器级优化”,并不要求实现全局代码布局优化、复杂指令调度或更高级的机器级分析框架。目标是在保证语义正确的前提下,让最终汇编更紧凑、更直接。
可选的两条常见实现路线如下:
1. 图着色寄存器分配
- 整体思路:把“两个虚拟寄存器若在某一程序点同时活跃,则不能分配到同一个物理寄存器”转化为图着色问题。图中的结点表示虚拟寄存器,边表示二者互相干涉;若有 `K` 个可分配物理寄存器,则目标是对干涉图进行 `K` 着色。
- 典型步骤:
1. 先对 MIR 做活跃性分析,计算各基本块或各指令位置的 live-in/live-out。
2. 根据活跃信息构建干涉图;若需要优化 move也可以额外记录可合并关系。
3. 按照可分配寄存器数 `K` 对图执行 simplify/select必要时结合启发式选择 spill 候选。
4. 若图可以着色,则回填每个虚拟寄存器对应的物理寄存器;若不能着色,则把选中的虚拟寄存器重写为 spill/reload 形式,并重新进行分析与分配。
5. 分配完成后,把使用到的 callee-saved 寄存器、额外 spill 栈槽等信息交给 `FrameLowering.cpp``AsmPrinter.cpp` 继续处理。
- 说明:图着色方法可以参考课堂 PPT 中介绍的基本思路来实现。实际工程里这类方法有很多变体,你也可以在这个大方向下结合自己的实现继续调整和优化具体细节;但无论采用哪种变体,都需要补齐活跃性分析、干涉图维护与 spill 重试机制等关键环节。
2. 线性扫描寄存器分配
- 整体思路:先把每个虚拟寄存器的活跃范围抽象为一个区间,再按照区间起点顺序扫描程序,动态维护当前正在占用物理寄存器的活跃区间集合;若出现寄存器不够用,再选择某个区间 spill。
- 典型步骤:
1. 为机器指令建立稳定顺序,并结合活跃性信息计算每个虚拟寄存器的 live interval。
2. 按区间起点排序后顺序扫描,维护当前仍然活跃的区间集合 `active`
3. 每处理到一个新区间时,先移除已经结束的区间并释放其占用的物理寄存器。
4. 若存在空闲物理寄存器,则直接分配;若没有空闲寄存器,则比较当前区间与 `active` 中已有区间的结束位置,选择 spill 当前区间或 spill 一个结束更晚的旧区间。
5. 对 spill 的虚拟寄存器插入 reload/store 后,需要重新计算受影响区间,再继续后续分配与汇编落地。
- 说明线性扫描通常更容易先做出一个可运行版本作为寄存器分配的起点也比较常见但如果要把效果做得更好仍然需要认真处理区间切分、调用点约束、callee/caller-saved 寄存器使用策略等问题。
无论采用图着色还是线性扫描,都不应把寄存器分配理解为“把虚拟寄存器简单替换成物理寄存器名字”。真正完整的实现还需要和 spill/reload、栈帧布局、callee-saved 保存恢复以及最终汇编输出联动,否则后端仍然无法支撑完整 SysY 程序。
## 5. 构建与运行
```bash
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j "$(nproc)"
```
## 6. Lab5 验证方式
项目编译后可先用当前示例用例检查后端链路是否仍能运行:
```bash
./build/bin/compiler --emit-asm test/test_case/functional/simple_add.sy
```
推荐继续使用统一脚本验证 “源码 -> 汇编 -> 可执行程序” 整体链路。`--run` 模式下会自动读取同名 `.in`,并将程序输出与退出码和同名 `.out` 比对,用于检查单个用例的完整结果:
```bash
./scripts/verify_asm.sh test/test_case/functional/simple_add.sy test/test_result/function/asm --run
```
建议在功能回归之外,再观察优化前后汇编输出差异。可按自己的实现方式保留调试日志、优化开关,或直接对比生成的汇编文本,重点关注:
1. 是否删除了明显冗余的 `move/copy` 指令。
2. 是否减少了不必要的 `load/store` 与重复访存。
3. 是否消除了无意义的恒等操作。
完成 Lab5 后,最终不应只停留在 `simple_add` 这一示例用例,而应对 `test/test_case` 下全部测试用例逐个回归,确保生成代码功能正确;如有需要,也可以自行编写批量测试脚本统一执行。在此基础上,再尽量减少不必要的 spill/reload、无效拷贝、冗余访存与低效机器指令以提升最终性能表现。

@ -1,78 +0,0 @@
# Lab6并行与循环优化
## 1. 本实验定位
Lab6 的重点是在 Lab4 基本标量优化之后,继续围绕循环结构开展更进一步的性能优化。本实验不再以补齐语义覆盖为主,而是把重点放在循环识别、循环变换以及可并行循环分析等问题上,为进一步提升最终生成代码的性能打基础。
## 2. Lab6 要求
本实验需要完成的事情包括:在现有 IR 上识别循环结构,能够区分循环头、循环体、前置块、退出块与回边等部分;实现有效的循环优化,并保证变换前后语义一致;将这些优化接入 `PassManager`,使其能够与 Lab4 的优化流程协同工作;最后通过回归测试和性能或代码规模对比,验证优化结果的正确性与收益。若希望进一步提升性能,也可以继续尝试可并行循环识别与并行化改造。
## 3. 相关文件
以下文件与本实验内容相关,建议优先阅读。
- `include/ir/IR.h`
- `src/ir/analysis/DominatorTree.cpp`
- `src/ir/analysis/LoopInfo.cpp`
- `src/ir/passes/PassManager.cpp`
## 4. 当前基础与前置准备
循环优化通常依赖一组相对稳定的基础分析,包括 CFG 与支配关系、循环层次信息(`LoopInfo`),以及 def-use/use-def 和副作用信息。只有这些基础信息足够稳定,后续的循环变换才不容易“优化错程序”。因此,在正式实现具体优化之前,建议先把分析链路与验证手段理顺。
## 5. 可实现的优化方向与实现提示
本实验可以选择的方向包括循环不变代码外提、归纳变量简化与强度削弱、循环展开、循环分裂,以及简单的并行化识别。如果你的实现还需要围绕当前框架补充其他循环相关优化,也可以按需扩展。
### 5.1 循环不变量外提Loop Invariant Code Motion
循环不变代码外提的核心,是把循环中每次迭代结果都不变的表达式移动到循环外执行。若某个表达式不依赖循环内变化的值,并且其操作数在循环体内不被改写,那么它就具备外提条件。这样做的直接收益,是减少循环体内的重复计算,降低迭代开销。实现时,通常需要先识别循环结构,再判断哪些表达式对所有迭代都恒定,然后把它们外提到循环前置块或其他等价安全位置。
### 5.2 强度削弱Strength Reduction
强度削弱的思路,是把高开销运算替换为等价的低开销运算。循环中的典型场景,是把乘法、除法等操作改写为递增或递减更新。这样可以降低每次迭代的算术成本,提高整体执行效率。实现时,通常需要先识别归纳变量以及与其线性相关的表达式,再判断是否可以通过引入辅助变量,用加减更新替代高成本运算。
### 5.3 循环展开Loop Unrolling
循环展开的做法,是在一次迭代中执行多份循环体副本,以减少控制指令比例,并提升指令级并行机会。它也常常能为后续向量化或流水线优化创造条件。实现时,需要选择合适的展开因子,复制循环体并调整步长;如果总迭代次数不能整除展开因子,还需要保留余数迭代路径以保证结果正确。
### 5.4 循环分裂Loop Fission
循环分裂是把一个包含多类语句的循环拆成多个循环,每个循环只执行原循环中的一部分语句。这样做通常有助于降低单个循环体的复杂度,改善数据局部性,并为并行化或向量化提供更好的前提。实现时,一般需要先做数据依赖分析;只有在若干语句之间不存在阻碍重排的依赖时,才适合将其拆分到不同循环中。
### 5.5 循环并行化Loop Parallelization
循环并行化的目标,是让不同迭代可以并发执行,以利用多核并行能力。它成立的前提,是迭代间不存在破坏语义的数据依赖。若分析结果表明循环可以并行,就可以进一步考虑任务划分、执行与归并,从而继续提升整体性能。不过,这一部分通常也有一定难度,对依赖分析、任务划分和执行正确性的要求都更高,因此更适合作为在前面优化基础上继续深入的方向。
## 6. 推荐实验流程
比较自然的推进顺序是:先跑通循环分析,再选择一种或几种循环优化逐步实现,然后接入 `PassManager`,最后结合测试与输出对比检查优化结果是否正确、是否带来预期收益。
## 7. 构建与验证
```bash
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j "$(nproc)"
```
### 7.1 功能回归
```bash
./scripts/verify_ir.sh test/test_case/functional/simple_add.sy test/test_result/function/ir --run
./scripts/verify_asm.sh test/test_case/functional/simple_add.sy test/test_result/function/asm --run
```
`--run` 模式下脚本会自动读取同名 `.in`,并将程序输出与退出码和同名 `.out` 比对。
完成 Lab6 后,不能只检查 `simple_add` 这类单个样例,而应对 `test/test_case` 下全部测试用例逐个回归;如有需要,也可以自行编写批量测试脚本统一执行。
### 7.2 优化效果对比(示例)
```bash
# 对比优化前后 IR/汇编输出(按你实现的开关或日志方式执行)
./build/bin/compiler --emit-ir test/test_case/functional/simple_add.sy
./build/bin/compiler --emit-asm test/test_case/functional/simple_add.sy
```
这里的 `simple_add` 只用于展示如何观察单个样例的输出差异;实际评估优化效果时,仍应结合更多测试用例,必要时覆盖全部测试集。

Binary file not shown.

Before

Width:  |  Height:  |  Size: 640 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 629 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 459 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 639 KiB

@ -1,234 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ $# -lt 1 ]]; then
echo "用法: $0 <input.sy> [output_dir] [--run] [-O]" >&2
exit 1
fi
input=$1
out_dir="test/test_result/asm"
run_exec=false
optimize=false
input_dir=$(dirname "$input")
shift
while [[ $# -gt 0 ]]; do
case "$1" in
--run)
run_exec=true
;;
-O|--optimize)
optimize=true
;;
*)
out_dir="$1"
;;
esac
shift
done
if [[ ! -f "$input" ]]; then
echo "输入文件不存在: $input" >&2
exit 1
fi
compiler="./build/bin/compiler"
if [[ ! -x "$compiler" ]]; then
echo "未找到编译器: $compiler ,请先构建。" >&2
exit 1
fi
if ! command -v aarch64-linux-gnu-gcc >/dev/null 2>&1; then
echo "未找到 aarch64-linux-gnu-gcc无法汇编/链接。" >&2
exit 1
fi
if ! command -v qemu-aarch64 >/dev/null 2>&1 && [[ "$run_exec" == true ]]; then
echo "未找到 qemu-aarch64无法运行生成的可执行文件。" >&2
exit 1
fi
find_runtime() {
if [[ -n "${SYSY_RUNTIME:-}" ]]; then
if [[ -f "$SYSY_RUNTIME" ]]; then
printf '%s\n' "$SYSY_RUNTIME"
return 0
fi
echo "环境变量 SYSY_RUNTIME 指向的文件不存在: $SYSY_RUNTIME" >&2
return 1
fi
local candidates=(
"./sylib/sylib.c"
"./sylib.c"
"./runtime/sylib.c"
"./lib/sylib.c"
)
local candidate
for candidate in "${candidates[@]}"; do
if [[ -f "$candidate" ]]; then
printf '%s\n' "$candidate"
return 0
fi
done
local found=""
found=$(find . \
-path './build' -prune -o \
-path './.git' -prune -o \
-type f -name 'sylib.c' -print | head -n 1)
if [[ -n "$found" ]]; then
printf '%s\n' "$found"
return 0
fi
return 1
}
# 获取当前时间戳(毫秒级)
get_timestamp_ms() {
if [[ "$OSTYPE" == "darwin"* ]]; then
if command -v gdate >/dev/null 2>&1; then
gdate +%s%3N
else
perl -MTime::HiRes -e 'printf "%d\n", Time::HiRes::time() * 1000' 2>/dev/null || date +%s%3N
fi
else
date +%s%3N 2>/dev/null || date +%s000
fi
}
runtime_src="$(find_runtime || true)"
if [[ -z "$runtime_src" ]]; then
echo "未找到运行时库源码 sylib.c" >&2
echo "可以显式指定SYSY_RUNTIME=/你的路径/sylib.c $0 <input.sy> [output_dir] [--run]" >&2
exit 1
fi
runtime_cache_dir="./build/test_runtime"
runtime_obj="$runtime_cache_dir/sylib.o"
mkdir -p "$runtime_cache_dir"
# 编译运行时库(如果需要)
if [[ ! -f "$runtime_obj" || "$runtime_src" -nt "$runtime_obj" ]]; then
aarch64-linux-gnu-gcc -O2 -c "$runtime_src" -o "$runtime_obj"
fi
mkdir -p "$out_dir"
base=$(basename "$input")
stem=${base%.sy}
asm_file="$out_dir/$stem.s"
exe="$out_dir/$stem"
stdin_file="$input_dir/$stem.in"
expected_file="$input_dir/$stem.out"
# 记录编译(生成汇编)开始时间
compile_start=$(get_timestamp_ms)
if [[ "$optimize" == true ]]; then
"$compiler" -O --emit-asm "$input" > "$asm_file"
else
"$compiler" --emit-asm "$input" > "$asm_file"
fi
# 记录编译结束时间
compile_end=$(get_timestamp_ms)
compile_time=$((compile_end - compile_start))
# 记录汇编+链接开始时间
assemble_start=$(get_timestamp_ms)
aarch64-linux-gnu-gcc "$asm_file" "$runtime_obj" -o "$exe"
# 记录汇编+链接结束时间
assemble_end=$(get_timestamp_ms)
assemble_time=$((assemble_end - assemble_start))
if [[ "$run_exec" == true ]]; then
stdout_file="$out_dir/$stem.stdout"
actual_file="$out_dir/$stem.actual.out"
set +e
exec_start=$(get_timestamp_ms)
if [[ -f "$stdin_file" ]]; then
qemu-aarch64 -L /usr/aarch64-linux-gnu -s 104857600 "$exe" < "$stdin_file" > "$stdout_file"
else
qemu-aarch64 -L /usr/aarch64-linux-gnu -s 104857600 "$exe" < /dev/null > "$stdout_file"
fi
status=$?
exec_end=$(get_timestamp_ms)
exec_time=$((exec_end - exec_start))
set -e
# 计算总时间
total_time=$((compile_time + assemble_time + exec_time))
# 保存运行时间到文件(只保存时间数值)
time_file="$out_dir/${stem}_time.txt"
echo "$total_time" > "$time_file"
# 保存输出(用于比对)
{
cat "$stdout_file"
if [[ -s "$stdout_file" ]] && (( $(tail -c 1 "$stdout_file" | wc -l) == 0 )); then
printf '\n'
fi
printf '%s\n' "$status"
} > "$actual_file"
# 比对输出
if [[ -f "$expected_file" ]]; then
temp_expected="$out_dir/$stem.expected.tmp"
temp_actual="$out_dir/$stem.actual.tmp"
if command -v python3 >/dev/null 2>&1; then
if python3 - "$expected_file" "$actual_file" <<'PY'
import sys
from pathlib import Path
def canon(path: str) -> bytes:
data = Path(path).read_bytes()
data = data.replace(b'\r\n', b'\n')
while data.endswith(b'\n'):
data = data[:-1]
lines = data.split(b'\n')
lines = [line.rstrip() for line in lines]
return b'\n'.join(lines)
sys.exit(0 if canon(sys.argv[1]) == canon(sys.argv[2]) else 1)
PY
then
rm -f "$temp_expected" "$temp_actual" 2>/dev/null
else
echo "输出不匹配: $expected_file" >&2
rm -f "$temp_expected" "$temp_actual" 2>/dev/null
exit 1
fi
else
tr -d '\r' < "$expected_file" > "$temp_expected"
tr -d '\r' < "$actual_file" > "$temp_actual"
if [[ -s "$temp_expected" ]] && (( $(tail -c 1 "$temp_expected" | wc -l) == 0 )); then
if [[ -s "$temp_actual" ]] && (( $(tail -c 1 "$temp_actual" | wc -l) == 1 )); then
truncate -s -1 "$temp_actual"
fi
fi
if diff -u "$temp_expected" "$temp_actual" > /dev/null 2>&1; then
rm -f "$temp_expected" "$temp_actual"
else
echo "输出不匹配: $expected_file" >&2
rm -f "$temp_expected" "$temp_actual"
exit 1
fi
fi
fi
fi

@ -1,157 +0,0 @@
#!/usr/bin/env bash
# ./scripts/verify_ir.sh test/test_case/functional/simple_add.sy test/test_result/function/ir --run
set -euo pipefail
if [[ $# -lt 1 ]]; then
echo "用法: $0 <input.sy> [output_dir] [--run] [-O]" >&2
exit 1
fi
out_dir="test/test_result/ir"
run_exec=false
optimize=false
input=""
while [[ $# -gt 0 ]]; do
case "$1" in
--run)
run_exec=true
;;
-O|--optimize)
optimize=true
;;
-*)
echo "未知选项: $1" >&2
exit 1
;;
*)
if [[ -z "$input" ]]; then
input="$1"
else
out_dir="$1"
fi
;;
esac
shift
done
if [[ -z "$input" ]]; then
echo "未指定输入文件" >&2
exit 1
fi
input_dir=$(dirname "$input")
if [[ ! -f "$input" ]]; then
echo "输入文件不存在: $input" >&2
exit 1
fi
compiler="./build/bin/compiler"
if [[ ! -x "$compiler" ]]; then
echo "未找到编译器: $compiler ,请先构建(如: mkdir -p build && cd build && cmake .. && make -j" >&2
exit 1
fi
mkdir -p "$out_dir"
base=$(basename "$input")
stem=${base%.sy}
out_file="$out_dir/$stem.ll"
stdin_file="$input_dir/$stem.in"
expected_file="$input_dir/$stem.out"
# 记录编译开始时间
compile_start=$(date +%s%N)
if [[ "$optimize" == true ]]; then
"$compiler" -O --emit-ir "$input" > "$out_file"
else
"$compiler" --emit-ir "$input" > "$out_file"
fi
# 记录编译结束时间
compile_end=$(date +%s%N)
compile_time=$(( ($compile_end - $compile_start) / 1000000 ))
if [[ "$run_exec" == true ]]; then
if ! command -v llc >/dev/null 2>&1; then
echo "未找到 llc无法运行 IR。请安装 LLVM。" >&2
exit 1
fi
if ! command -v clang >/dev/null 2>&1; then
echo "未找到 clang无法链接可执行文件。请安装 LLVM/Clang。" >&2
exit 1
fi
obj="$out_dir/$stem.o"
exe="$out_dir/$stem"
stdout_file="$out_dir/$stem.stdout"
actual_file="$out_dir/$stem.actual.out"
sylib_obj="$out_dir/sylib.o"
# 记录链接开始时间
link_start=$(date +%s%N)
if [[ ! -f "$sylib_obj" ]]; then
gcc -c sylib/sylib.c -o "$sylib_obj"
fi
llc -O0 -filetype=obj "$out_file" -o "$obj"
clang "$obj" "$sylib_obj" -o "$exe"
# 记录链接结束时间
link_end=$(date +%s%N)
link_time=$(( ($link_end - $link_start) / 1000000 ))
# 记录执行开始时间
exec_start=$(date +%s%N)
set +e
if [[ -f "$stdin_file" ]]; then
(ulimit -s 65536 && "$exe" < "$stdin_file" > "$stdout_file")
else
(ulimit -s 65536 && "$exe" > "$stdout_file")
fi
status=$?
set -e
# 记录执行结束时间
exec_end=$(date +%s%N)
exec_time=$(( ($exec_end - $exec_start) / 1000000 ))
# 保存运行时间到文件(只保存执行时间,不包含编译和链接时间)
time_file="$out_dir/${stem}_time.txt"
echo "$exec_time" > "$time_file"
# 保存输出(用于比对)
{
cat "$stdout_file"
# 确保 stdout 以换行结尾
if [[ -s "$stdout_file" ]] && (( $(tail -c 1 "$stdout_file" | wc -l) == 0 )); then
printf '\n'
fi
printf '%s\n' "$status"
} > "$actual_file"
# 比对输出
if [[ -f "$expected_file" ]]; then
temp_expected="$out_dir/$stem.expected.tmp"
temp_actual="$out_dir/$stem.actual.tmp"
tr -d '\r' < "$expected_file" > "$temp_expected"
tr -d '\r' < "$actual_file" > "$temp_actual"
if [[ -s "$temp_expected" ]] && (( $(tail -c 1 "$temp_expected" | wc -l) == 0 )); then
if [[ -s "$temp_actual" ]] && (( $(tail -c 1 "$temp_actual" | wc -l) == 1 )); then
truncate -s -1 "$temp_actual"
fi
fi
if diff -u "$temp_expected" "$temp_actual" > /dev/null 2>&1; then
rm -f "$temp_expected" "$temp_actual"
else
echo "输出不匹配: $expected_file" >&2
rm -f "$temp_expected" "$temp_actual"
exit 1
fi
fi
fi
Loading…
Cancel
Save