diff --git a/README.md b/README.md index ca3b0c4..9cb4ab6 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # SysY 编译器课程实验(C++) 本仓库为“并行编译课程实验”提供一个 SysY 编译器的最小可运行示例,实验按 Lab1–Lab6 逐步完成: -从前端(词法/语法分析与语法树处理)到中端(IR 生成与优化),再到后端(ARM64/AArch64 汇编生成、寄存器分配与后端优化),最后进行循环/并行相关优化。 +从前端(词法/语法分析与语法树处理)到中端(IR 生成、基本标量优化),再到后端(ARM64/AArch64 汇编生成、寄存器分配与后端优化),最后进行循环/并行相关优化。 ## 1. 实验介绍 @@ -10,8 +10,8 @@ | Lab1 | 语法树构建 | 基于 SysY 源程序完成语法分析与语法树构建,并按约定输出语法树 | | Lab2 | 中间表示生成 | 将语法树翻译为 LLVM 风格的中间表示(IR),并输出 IR | | Lab3 | 指令选择与汇编生成 | 将 IR 翻译为目标平台汇编代码(本项目以 ARM64/AArch64 为主) | -| Lab4 | 寄存器分配与后端优化 | 为后端生成的虚拟寄存器分配物理寄存器,并完成 spill/reload、冗余指令消除与局部后端优化 | -| Lab5 | 基本标量优化 | 实现常见的标量优化(如常量传播、死代码删除、简化 CFG 等) | +| Lab4 | 基本标量优化 | 实现常见的标量优化(如常量传播、死代码删除、简化 CFG 等) | +| Lab5 | 寄存器分配与后端优化 | 为后端生成的虚拟寄存器分配物理寄存器,并完成 spill/reload、冗余指令消除与局部后端优化 | | Lab6 | 并行与循环优化 | 面向循环的优化(循环变换/并行化等),进一步提升程序性能 | ## 2. 实验环境配置 diff --git a/doc/Lab4-基本标量优化.md b/doc/Lab4-基本标量优化.md new file mode 100644 index 0000000..0f31fa8 --- /dev/null +++ b/doc/Lab4-基本标量优化.md @@ -0,0 +1,111 @@ +# 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/function/simple_add.sy +``` + +这条命令只适合先观察单个样例的 IR 形态。完成 Lab4 后,不能只检查 `simple_add`,还应覆盖 `test/test_case` 下全部测试用例。 + +### 6.2 语义回归 + + ```bash +./scripts/verify_ir.sh test/test_case/function/simple_add.sy test/test_result/function/ir --run +./scripts/verify_asm.sh test/test_case/function/simple_add.sy test/test_result/function/asm --run + ``` + +目标:脚本自动读取同名 `.in`,并将程序输出与退出码和同名 `.out` 比对,确保优化后程序行为与优化前保持一致。 +完成 Lab4 后,应对 `test/test_case` 下全部测试用例逐个回归;如有需要,也可以自行编写批量测试脚本统一执行。 diff --git a/doc/Lab5-寄存器分配.md b/doc/Lab5-寄存器分配.md new file mode 100644 index 0000000..4760a77 --- /dev/null +++ b/doc/Lab5-寄存器分配.md @@ -0,0 +1,97 @@ +# 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/function/simple_add.sy +``` + +推荐继续使用统一脚本验证 “源码 -> 汇编 -> 可执行程序” 整体链路。`--run` 模式下会自动读取同名 `.in`,并将程序输出与退出码和同名 `.out` 比对,用于检查单个用例的完整结果: + +```bash +./scripts/verify_asm.sh test/test_case/function/simple_add.sy test/test_result/function/asm --run +``` + +建议在功能回归之外,再观察优化前后汇编输出差异。可按自己的实现方式保留调试日志、优化开关,或直接对比生成的汇编文本,重点关注: + +1. 是否删除了明显冗余的 `move/copy` 指令。 +2. 是否减少了不必要的 `load/store` 与重复访存。 +3. 是否消除了无意义的恒等操作。 + +完成 Lab5 后,最终不应只停留在 `simple_add` 这一示例用例,而应对 `test/test_case` 下全部测试用例逐个回归,确保生成代码功能正确;如有需要,也可以自行编写批量测试脚本统一执行。在此基础上,再尽量减少不必要的 spill/reload、无效拷贝、冗余访存与低效机器指令,以提升最终性能表现。 diff --git a/doc/Lab6-并行与循环优化.md b/doc/Lab6-并行与循环优化.md index e1cc137..4f6efd7 100644 --- a/doc/Lab6-并行与循环优化.md +++ b/doc/Lab6-并行与循环优化.md @@ -2,11 +2,11 @@ ## 1. 本实验定位 -Lab6 的重点是在 Lab5 基本标量优化之后,继续围绕循环结构开展更进一步的性能优化。本实验不再以补齐语义覆盖为主,而是把重点放在循环识别、循环变换以及可并行循环分析等问题上,为进一步提升最终生成代码的性能打基础。 +Lab6 的重点是在 Lab4 基本标量优化之后,继续围绕循环结构开展更进一步的性能优化。本实验不再以补齐语义覆盖为主,而是把重点放在循环识别、循环变换以及可并行循环分析等问题上,为进一步提升最终生成代码的性能打基础。 ## 2. Lab6 要求 -本实验需要完成的事情包括:在现有 IR 上识别循环结构,能够区分循环头、循环体、前置块、退出块与回边等部分;实现有效的循环优化,并保证变换前后语义一致;将这些优化接入 `PassManager`,使其能够与 Lab5 的优化流程协同工作;最后通过回归测试和性能或代码规模对比,验证优化结果的正确性与收益。若希望进一步提升性能,也可以继续尝试可并行循环识别与并行化改造。 +本实验需要完成的事情包括:在现有 IR 上识别循环结构,能够区分循环头、循环体、前置块、退出块与回边等部分;实现有效的循环优化,并保证变换前后语义一致;将这些优化接入 `PassManager`,使其能够与 Lab4 的优化流程协同工作;最后通过回归测试和性能或代码规模对比,验证优化结果的正确性与收益。若希望进一步提升性能,也可以继续尝试可并行循环识别与并行化改造。 ## 3. 相关文件 diff --git a/src/ir/passes/CSE.cpp b/src/ir/passes/CSE.cpp index 58b8bdd..4b24dd0 100644 --- a/src/ir/passes/CSE.cpp +++ b/src/ir/passes/CSE.cpp @@ -1,5 +1,4 @@ // 公共子表达式消除(CSE): // - 识别并复用重复计算的等价表达式 // - 典型放置在 ConstFold 之后、DCE 之前 -// - 当前为 Lab5 的框架占位,具体算法由实验实现 - +// - 当前为 Lab4 的框架占位,具体算法由实验实现