From abcae5866189e86d605b4d5cb5be96d6ac4a6340 Mon Sep 17 00:00:00 2001 From: tangttangtang <206374282@qq.com> Date: Sun, 12 Apr 2026 19:44:00 +0800 Subject: [PATCH] =?UTF-8?q?vector=5Fmul3=E6=B5=8B=E8=AF=95=E5=B7=B2?= =?UTF-8?q?=E9=80=9A=E8=BF=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/Lab3-指令选择与汇编生成.md | 523 +++++++++++++++--------- src/mir/AsmPrinter.cpp | 50 ++- 2 files changed, 385 insertions(+), 188 deletions(-) diff --git a/doc/Lab3-指令选择与汇编生成.md b/doc/Lab3-指令选择与汇编生成.md index 2211204..dd6aef1 100644 --- a/doc/Lab3-指令选择与汇编生成.md +++ b/doc/Lab3-指令选择与汇编生成.md @@ -1,19 +1,19 @@ # Lab3:指令选择与汇编生成说明 -## 1. 文档目标 +## 1. 文档范围 -本文档说明当前仓库中 Lab3 后端的真实实现方案,重点回答四个问题: +本文档描述当前仓库中 Lab3 后端的真实实现,而不是计划中的设计。内容覆盖以下四部分: -1. Lab3 后端的整体流水线是什么。 -2. 当前实现与 `Reference` 目录下三份资料的对应关系是什么。 -3. 指令选择、寄存器分配、栈布局分别是如何落地的。 -4. 当前测试结果收敛到了什么程度。 +- Lab3 后端的整体流水线与模块划分 +- 当前实现与 `Reference` 目录下三份资料的对应关系 +- 近期关键正确性问题的定位与修复 +- 当前测试规范与最新测试结论 -本文档只描述仓库当前代码,不把“计划中的优化”写成“已经完成的实现”。 +本文档对应的是仓库当前代码状态。 --- -## 2. 参考依据 +## 2. 参考资料与采用方式 Lab3 当前实现主要参考以下三份资料: @@ -21,52 +21,52 @@ Lab3 当前实现主要参考以下三份资料: - `Reference/lecture05-instruction selection-169.pdf` - `Reference/lecture11-register allocation-part2-169.pdf` -对这三份资料的吸收方式如下: +这三份资料在项目中的落点分别如下: -- 栈布局与函数调用约定参考 `lab03` - 采用 AArch64 / AAPCS64 基本规则,使用 `x29` 作为帧指针,栈帧按 16 字节对齐,区分 caller-saved 与 callee-saved 寄存器。 -- 指令选择参考 `lecture05` - 当前实现采用“宏扩展式 lowering + 局部 peephole / 融合优化”的工程化方案,而不是完整树覆盖或 SelectionDAG 风格的树模式匹配。 -- 寄存器分配参考 `lecture11` - 当前实现采用 George 风格图着色寄存器分配,包含 `build / simplify / coalesce / freeze / spill / select` 这一套核心流程。 +- `lab03` + 主要对应栈布局、函数序言和尾声、AAPCS64 调用约定、栈上传参和 16 字节对齐。 +- `lecture05` + 主要对应 instruction selection 的方法论。当前仓库采用的是“宏扩展式 lowering + 局部模式融合”的工程化方案,而不是完整树覆盖或 SelectionDAG。 +- `lecture11` + 主要对应寄存器分配。当前仓库使用的是 George 风格图着色分配,而不是线性扫描。 -因此,当前项目不是“逐页复刻讲义代码”,而是在讲义方法论基础上完成了适合本仓库的实现。 +因此,当前实现不是逐页照搬讲义,而是按讲义方法论落到本项目结构中。 --- -## 3. 当前后端总览 +## 3. 后端整体流水线 -当前 `compiler --emit-asm` 的真实执行流程如下: +当前 `compiler --emit-asm` 的主流程如下: -1. ANTLR 前端解析源程序。 -2. 语义分析构建符号与类型信息。 -3. IR 生成得到 LLVM 风格中间表示。 -4. IR Pass Pipeline 做中端优化。 +1. 前端基于 ANTLR 解析 SysY 源程序。 +2. 语义分析建立类型和符号信息。 +3. IR 生成阶段产出 LLVM 风格中间表示。 +4. IR Pass Pipeline 做中端标量优化。 5. `LowerToMIR` 将 IR 降到自定义 MIR。 6. `RunRegAlloc` 对 MIR 虚拟寄存器做图着色分配。 -7. `RunFrameLowering` 计算栈对象偏移与最终栈帧大小。 +7. `RunFrameLowering` 计算栈对象偏移和最终帧大小。 8. `PrintAsm` 输出 AArch64 汇编。 -对应入口见 [src/main.cpp](/home/hw/nudt-compiler-cpp/src/main.cpp:1)。 +入口在 [src/main.cpp](../src/main.cpp)。 -这意味着当前 Lab3 后端已经是项目内自研后端,不依赖外部 LLVM 后端来生成目标汇编。 +这意味着 Lab3 已经不依赖 LLVM 后端生成汇编,而是使用仓库内自研的 MIR 后端。 --- -## 4. 模块划分 +## 4. 核心模块划分 ### 4.1 MIR 基础设施 核心文件: -- [include/mir/MIR.h](/home/hw/nudt-compiler-cpp/include/mir/MIR.h:1) -- [src/mir/MIRInstr.cpp](/home/hw/nudt-compiler-cpp/src/mir/MIRInstr.cpp:1) -- [src/mir/MIRBasicBlock.cpp](/home/hw/nudt-compiler-cpp/src/mir/MIRBasicBlock.cpp:1) -- [src/mir/MIRFunction.cpp](/home/hw/nudt-compiler-cpp/src/mir/MIRFunction.cpp:1) -- [src/mir/MIRContext.cpp](/home/hw/nudt-compiler-cpp/src/mir/MIRContext.cpp:1) -- [src/mir/Register.cpp](/home/hw/nudt-compiler-cpp/src/mir/Register.cpp:1) +- [include/mir/MIR.h](../include/mir/MIR.h) +- [src/mir/MIRInstr.cpp](../src/mir/MIRInstr.cpp) +- [src/mir/MIRBasicBlock.cpp](../src/mir/MIRBasicBlock.cpp) +- [src/mir/MIRFunction.cpp](../src/mir/MIRFunction.cpp) +- [src/mir/MIRContext.cpp](../src/mir/MIRContext.cpp) +- [src/mir/Register.cpp](../src/mir/Register.cpp) -这一层定义了后端使用的核心对象: +这一层定义了: - `MachineOperand` - `AddressExpr` @@ -77,48 +77,56 @@ Lab3 当前实现主要参考以下三份资料: - `StackObject` - `Allocation` -当前 MIR 能表达: +当前 MIR 能表达的核心语义包括: - 整数算术与位运算 - 浮点算术 -- `load/store` -- `lea` -- 比较与条件跳转 -- 函数调用与返回 +- 比较、跳转与返回 +- `load/store/lea` +- 函数调用 - `memset` -- 类型转换 +- 整浮转换 + +MIR 的作用不是完全等价于 AArch64 汇编,而是作为“比 IR 更接近机器、但仍保留寄存器和地址表达式抽象”的中间层,便于后续做寄存器分配和栈帧落地。 ### 4.2 IR 到 MIR 的 lowering 核心文件: -- [src/mir/Lowering.cpp](/home/hw/nudt-compiler-cpp/src/mir/Lowering.cpp:1) +- [src/mir/Lowering.cpp](../src/mir/Lowering.cpp) 职责包括: - IR 指令到 MIR 指令的逐条翻译 - `alloca` 到栈对象的转换 - `load/store/gep` 到地址表达式的转换 -- `phi` 结点结果预分配与并行 copy lowering -- 直接调用的 MIR 生成 +- `phi` 结点预分配与并行 copy lowering +- 控制流和分支的 MIR 化 +- 直接调用的 MIR 构造 ### 4.3 寄存器分配 核心文件: -- [src/mir/RegAlloc.cpp](/home/hw/nudt-compiler-cpp/src/mir/RegAlloc.cpp:1) +- [src/mir/RegAlloc.cpp](../src/mir/RegAlloc.cpp) + +当前实现的是 `GeorgeColoringAllocator`,负责: -当前实现的是 `GeorgeColoringAllocator`,不是线性扫描。 +- 活跃性分析 +- 干涉图构建 +- move-related coalescing +- spill 选择 +- 颜色分配 ### 4.4 栈帧与对象布局 核心文件: -- [src/mir/FrameLowering.cpp](/home/hw/nudt-compiler-cpp/src/mir/FrameLowering.cpp:1) +- [src/mir/FrameLowering.cpp](../src/mir/FrameLowering.cpp) -职责包括: +这一层负责: -- 局部栈对象布局 +- 局部对象布局 - spill 槽布局 - callee-saved 保存槽布局 - 栈对象偏移计算 @@ -128,245 +136,392 @@ Lab3 当前实现主要参考以下三份资料: 核心文件: -- [src/mir/AsmPrinter.cpp](/home/hw/nudt-compiler-cpp/src/mir/AsmPrinter.cpp:1) +- [src/mir/AsmPrinter.cpp](../src/mir/AsmPrinter.cpp) -职责包括: +这一层负责: -- MIR 到 AArch64 汇编文本的最终选择 -- 地址模式发射 +- MIR 到 AArch64 汇编文本的最终映射 +- 地址模式选择 - 调用约定落地 -- 序言 / 尾声生成 +- 序言和尾声生成 - 全局变量与常量区输出 +从实现风格上说,真正的“最终 instruction selection”并不只发生在 `Lowering.cpp`,而是由 `Lowering.cpp` 和 `AsmPrinter.cpp` 共同完成。 + --- -## 5. 与 `lab03` 的对应关系 +## 5. IR 到 MIR 的实现方式 + +### 5.1 标量指令 lowering + +在 [src/mir/Lowering.cpp](../src/mir/Lowering.cpp) 中,以下 IR 会逐条映射成 MIR: + +- `Add/Sub/Mul/Div/Rem` +- `FAdd/FSub/FMul/FDiv/FNeg` +- `ICmp/FCmp` +- `Zext/IToF/FtoI` +- `Call/Ret` +- `Br/CondBr` + +这种做法对应 `lecture05` 中的 macro-expansion / one-by-one translation。 -`lab03` 的关键点是“基于宏扩展、自顶向下逐条翻译、正确实现 ARMv8 调用约定与栈帧”。 +### 5.2 地址表达式 lowering -当前实现与其对应关系如下: +内存访问不会立即固定成某一条 AArch64 指令,而是先保存在 `AddressExpr` 中。它可以表达: -- 自顶向下逐条翻译:符合 - 在 [src/mir/Lowering.cpp](/home/hw/nudt-compiler-cpp/src/mir/Lowering.cpp:1) 中,IR 指令被逐条转换为 MIR。 -- 栈帧按 16 字节对齐:符合 - 在 [src/mir/FrameLowering.cpp](/home/hw/nudt-compiler-cpp/src/mir/FrameLowering.cpp:1) 中对最终 `frame_size` 做了 16 字节对齐。 -- 使用 `x29` 作为帧指针:符合 - 在 [src/mir/AsmPrinter.cpp](/home/hw/nudt-compiler-cpp/src/mir/AsmPrinter.cpp:889) 附近可以看到标准的 `stp x29, x30`、`mov x29, sp`、`mov sp, x29`、`ldp x29, x30`。 -- 整型参数 / 返回值遵守 AAPCS64:符合 - 整型参数按 `x0-x7`,浮点参数按 `v0-v7`,超出部分走栈,见 [src/mir/AsmPrinter.cpp](/home/hw/nudt-compiler-cpp/src/mir/AsmPrinter.cpp:843) 起的参数位置计算逻辑。 -- caller-saved / callee-saved 区分:符合 - 当前分配器主要使用 `x19-x28` 与 `v8-v15` 作为可分配寄存器,并在需要时保存恢复。 +- 基址来自栈对象 +- 基址来自全局符号 +- 基址来自寄存器 +- 常量偏移 +- 缩放索引寄存器 -因此,从“课程实验的基础代码生成要求”来看,当前实现总体符合 `lab03` 的设计方向。 +这样做的好处是: + +- `getelementptr` 可以先降成统一地址表达式 +- 寄存器分配完成后再决定能否发成直接 indexed addressing +- `lea + load/store` 是否融合可以推迟到汇编打印阶段 + +### 5.3 `phi` lowering + +`phi` 不是直接发成 MIR 指令,而是在 lowering 时分两步处理: + +1. 先为每个 `phi` 结果预分配目标 vreg。 +2. 再按 CFG 边收集 copy,并在前驱边上发射并行 copy。 + +对于条件跳转前驱,如果直接在原块尾部插入 copy 可能破坏 terminator 结构,因此实现里会在需要时插入专用边块。 + +这是 Lab3 正确性最关键的一部分之一,后文会专门说明修复细节。 --- -## 6. 与 `lecture05` 的对应关系 +## 6. 指令选择实现说明 -`lecture05` 讲的是 instruction selection 的方法论,尤其区分了: +### 6.1 与 `lecture05` 的关系 + +`lecture05` 讲的是 instruction selection 的三类主要思路: - 宏扩展 - 树模式匹配 - 窥孔优化 -当前实现最接近下面这条路线: - -- 先做宏扩展式 lowering - 将 IR 指令逐条翻译成 MIR 指令。 -- 再做局部模式融合 - 在汇编打印阶段把 MIR 指令组合成更自然的 AArch64 指令序列。 +当前仓库最接近的路线是: -这体现在两层: +- 先做宏扩展 lowering +- 再在汇编发射阶段做局部模式融合 -### 6.1 宏扩展 lowering +因此,当前实现符合 `lecture05` 的思想范围,但不是树覆盖式 instruction selector。 -在 [src/mir/Lowering.cpp](/home/hw/nudt-compiler-cpp/src/mir/Lowering.cpp:1) 中,以下 IR 指令会直接降成对应 MIR: +### 6.2 当前实际做了哪些选择和融合 -- `Add/Sub/Mul/Div/Rem` -- `FAdd/FSub/FMul/FDiv` -- `ICmp/FCmp` -- `Zext/IToF/FtoI` -- `Load/Store` -- `GetElementPtr` -- `Call/Return` +在 [src/mir/AsmPrinter.cpp](../src/mir/AsmPrinter.cpp) 中,当前已经实现了多类工程化 instruction selection: -这种做法符合讲义中“macro-expansion / one-by-one translation”的基本思路。 +- `icmp/fcmp + condbr` 的融合发射 +- `lea + load/store` 的直接访存融合 +- 基址加缩放索引的直接寻址 +- `add/sub` 的立即数特化 +- `rem` 到 `sdiv + msub` 的展开 +- 立即数物化到寄存器 +- spill/load/store 到统一的帧地址访问 -### 6.2 局部模式融合 +因此,Lab3 当前不是“先生成一份一比一 MIR,再无脑打印汇编”,而是保留了机器相关的组合空间。 -在 [src/mir/AsmPrinter.cpp](/home/hw/nudt-compiler-cpp/src/mir/AsmPrinter.cpp:699) 之后,当前实现做了若干局部优化: +### 6.3 当前实现与 LLVM 后端的差异 -- 比较 + 条件跳转融合 -- `lea + load/store` 融合 -- 直接 indexed addressing 发射 -- `add/sub` 的立即数特化 -- `rem` 选择为 `sdiv + msub` +虽然当前全量样例已经通过,但代码生成质量和 LLVM 后端仍然不是同一层级。当前实现仍然有这些特征: -因此,当前实现属于: +- 没有完整树模式匹配 +- 没有 SelectionDAG 或 GlobalISel +- 没有大规模机器级组合优化 +- 可分配寄存器集合偏保守 -- 符合 `lecture05` 的宏扩展与局部模式优化思想 -- 但不是完整的树模式匹配 / 瓦片平铺实现 +所以更准确的描述是: -如果要求“完全按树覆盖方式实现 instruction selection”,则当前实现并不属于那一类;但如果要求“以讲义方法论为参考完成工程化指令选择”,则当前实现是符合的。 +- 当前实现已经满足 Lab3 的正确性与基本性能要求 +- 但不是 LLVM 级别的工业后端 --- -## 7. 与 `lecture11` 的对应关系 +## 7. 调用约定与栈布局 -这部分是当前实现与参考资料对齐程度最高的一部分。 +### 7.1 与 `lab03` 的关系 -在 [src/mir/RegAlloc.cpp](/home/hw/nudt-compiler-cpp/src/mir/RegAlloc.cpp:166) 中,当前使用 `GeorgeColoringAllocator` 完成寄存器分配。核心步骤包括: +`lab03` 的重点是: -1. 基于基本块的 `use/def/live_in/live_out` 活跃性分析 -2. 构建干涉图 -3. 收集 `Copy` 指令形成 move 关系 -4. `simplify` -5. `coalesce` -6. `freeze` -7. `select spill` -8. `assign colors` -9. spill 槽分配与最终 `Allocation` 提交 +- 正确的 AArch64 / AAPCS64 调用约定 +- 正确的栈帧构造 +- 16 字节对齐 +- caller-saved 与 callee-saved 的区分 -这和 `lecture11` 中 George 改进算法的讲义结构是一致的。 +当前仓库在这些点上总体是符合的。 -当前具体策略还包括: +### 7.2 当前调用约定实现 -- GPR / FPR 分开着色 -- spill cost 按基本块权重估计 -- 被分配到 callee-saved 的物理寄存器会记录到函数对象中,供后续序言 / 尾声保存恢复 +参数与返回值规则主要由 [src/mir/AsmPrinter.cpp](../src/mir/AsmPrinter.cpp) 负责落地。 -因此,寄存器分配部分可以判断为: +当前已经实现: -- 不只是“概念上参考 George” -- 而是代码结构上已经明显按 George 图着色路线实现 +- 整型参数优先使用 `x0-x7` +- 浮点参数优先使用 `s0-s7` +- 超出寄存器容量的参数走栈 +- 整型返回值走 `x0` +- 浮点返回值走 `s0` +- 调用前按需要扩栈,调用后回收 ---- +形参接收通过 `MachineInstr::Arg` 发射,调用点搬参与返回值接收通过 `MachineInstr::Call` 发射。 -## 8. 栈布局实现说明 +### 7.3 当前栈对象来源 -当前栈对象来源主要有三类: +栈对象主要来自三类: - `alloca` 降低得到的局部对象 - 寄存器分配产生的 spill 槽 -- 需要保存的 callee-saved 寄存器槽 +- 被使用到的 callee-saved 寄存器保存槽 + +### 7.4 当前帧布局方式 -布局过程在 [src/mir/FrameLowering.cpp](/home/hw/nudt-compiler-cpp/src/mir/FrameLowering.cpp:1) 中完成,基本策略为: +在 [src/mir/FrameLowering.cpp](../src/mir/FrameLowering.cpp) 中,当前布局策略为: -1. 遍历栈对象列表。 -2. 按对象对齐要求推进 `cursor`。 -3. 将对象偏移记为相对 `x29` 的负偏移。 -4. 最终将 `frame_size` 向上对齐到 16 字节。 +1. 遍历所有栈对象 +2. 按对象对齐要求推进 `cursor` +3. 记录相对帧指针的对象偏移 +4. 将最终 `frame_size` 向上对齐到 16 字节 -汇编层使用方式如下: +在汇编发射时: -- 栈对象地址通过 `EmitFrameAddress` 生成。 -- spill load/store 使用 `x17` 作为内部临时地址寄存器。 -- 序言中先保存 `x29/x30`,再下移 `sp`,再保存实际使用到的 callee-saved 寄存器。 +- `x29` 作为帧指针 +- `x30` 作为返回地址寄存器 +- 需要保存的 callee-saved GPR/FPR 会出现在序言和尾声中 +- spill/load/store 通过统一的帧地址访问例程发射 -这与 `lab03` 文档中的 ARMv8 栈帧思路是一致的。 +### 7.5 当前寄存器选择策略对调用的影响 + +当前寄存器分配器对 GPR 主要使用 `x19-x28`,对 FPR 主要使用 `s8-s15`。这是一种偏保守但稳定的策略,优点是: + +- 调用边界更容易处理 +- caller-saved 污染更少 +- 实现复杂度低 + +代价是: + +- 可分配寄存器集合比 LLVM 更小 +- 高压代码里更容易 spill --- -## 9. 调用约定实现说明 +## 8. George 图着色寄存器分配 -当前 AArch64 调用约定的落地主要在 [src/mir/AsmPrinter.cpp](/home/hw/nudt-compiler-cpp/src/mir/AsmPrinter.cpp:843) 之后。 +这部分与 `lecture11` 的对应关系最强。 -已经实现的规则包括: +在 [src/mir/RegAlloc.cpp](../src/mir/RegAlloc.cpp) 中,当前实现包含以下典型步骤: -- 整型参数优先使用 `x0-x7` -- 浮点参数优先使用 `v0-v7` -- 超过寄存器容量的参数按栈传递 -- 整型返回值走 `x0` -- 浮点返回值走 `s0` -- 栈上传参调用前先扩栈,调用后回收 +1. 基本块级 `use/def/live_in/live_out` 活跃性分析 +2. 干涉图构建 +3. `Copy` 指令诱导的 move 关系收集 +4. `simplify` +5. `coalesce` +6. `freeze` +7. `select spill` +8. `assign colors` +9. spill 槽创建与最终 `Allocation` 提交 -当前设计中: +当前实现还有几个重要特征: -- 参数位置计算由 `ComputeArgLocation` 完成 -- 调用点参数搬运在 `MachineInstr::Call` 的汇编发射中完成 -- 形参读取在 `MachineInstr::Arg` 的汇编发射中完成 +- GPR 和 FPR 分开着色 +- spill cost 会参考基本块权重 +- 分配到 callee-saved 的物理寄存器会记录回函数对象,供后续序言和尾声保存恢复 -整体上已经满足 Lab3 基础调用约定要求。 +因此,这里不是“概念上参考了图着色”,而是代码结构上就已经沿着 George 算法在实现。 --- -## 10. 近期关键修正 +## 9. 近期关键正确性修复 + +### 9.1 `phi` 并行 copy 修复 + +修复位置: + +- [src/mir/Lowering.cpp](../src/mir/Lowering.cpp) + +原始问题是:多个 `phi copy` 被按普通顺序赋值发射,旧值可能在后续 copy 使用前就被提前覆盖。 + +这在复杂循环头里会表现为: -近期最重要的 Lab3 正确性修正发生在 [src/mir/Lowering.cpp](/home/hw/nudt-compiler-cpp/src/mir/Lowering.cpp:1) 的 `phi` lowering。 +- `a' <- t` +- `b' <- a` +- `d' <- c` +- `e' <- d` -### 10.1 修正前的问题 +如果先发 `a' <- t`,后面的 `b' <- a` 读到的就不是旧 `a`,而是已经被覆盖的新值。 -原先 `phi` lowering` 会把前驱到后继的 copy 当作普通顺序赋值处理。 -在复杂循环头中,多个 `phi` 之间存在“旧值仍要被后续 copy 使用”的情况,如果拷贝顺序错误,就会提前覆盖旧值,生成错误结果。 +当前修复后的策略是: -这一问题直接导致了: +- 先按 CFG 边收集所有 `phi copy` +- 优先发“目的寄存器不再被其他待发 copy 当作源使用”的 copy +- 如有环,则引入临时 vreg 打破 +- 对条件边在必要时插入专用边块 + +这个问题直接导致过: - `crypto-1.sy` - `crypto-2.sy` - `crypto-3.sy` -三个高性能样例的汇编结果错误,而对应 IR 结果是正确的。 +修复后,这三个样例已经恢复通过。 + +### 9.2 有序浮点比较的 NaN 语义修复 + +修复位置: + +- [src/mir/AsmPrinter.cpp](../src/mir/AsmPrinter.cpp) + +这个问题比表面上看起来更隐蔽。IR 层的浮点比较打印是: + +- `FCmpEQ -> fcmp oeq` +- `FCmpNE -> fcmp one` +- `FCmpLT -> fcmp olt` +- `FCmpGT -> fcmp ogt` +- `FCmpLE -> fcmp ole` +- `FCmpGE -> fcmp oge` + +见 [src/ir/IRPrinter.cpp](../src/ir/IRPrinter.cpp)。 + +这里的关键字是 `ordered`。也就是说,比较一旦遇到 `NaN`,这些条件不应该按普通整数式条件码去理解。 + +原来的 Lab3 后端把 `FCmp` 的结果物化和 `FCmp + CondBr` 融合分支都简单映射成了: + +- `eq/ne/lt/gt/le/ge` + +这会在 AArch64 上引入错误的 `NaN` 语义。对照 LLVM AArch64 后端后,当前修正为: + +- `oeq -> eq` +- `olt -> mi` +- `ogt -> gt` +- `ole -> ls` +- `oge -> ge` +- `one -> 复合逻辑,不是单一条件码` + +也就是说,浮点比较不能直接照抄整数比较的条件码名称。 + +### 9.3 `vector_mul3` 超时的真实原因 + +`vector_mul3` 最开始表现为超时,很容易误判成: + +- 热点循环代码生成太慢 +- spill 太多 +- 指令选择不够激进 + +但实际定位后发现,真正原因不是主循环慢,而是浮点比较语义错了。 -### 10.2 修正内容 +定位过程中的关键事实有两点: -当前 `phi` lowering` 已改为: +- Lab2 全量 `214 PASS / 0 FAIL` +- `vector_mul3` 在 Lab2 不超时 -- 先按边收集 `phi` copy` -- 对条件跳转前驱建立专门的边块 -- 按并行 copy 规则发射 -- 优先发“目的寄存器不再被其他 copy 当作源使用”的 copy -- 如果形成环,则使用临时 vreg 打破 +对应日志见 [output/logs/lab2/lab2_20260412_183222/whole.log](../output/logs/lab2/lab2_20260412_183222/whole.log)。 -这个修正已经使 `crypto-1/2/3` 三个样例重新通过。 +这说明: + +- 算法本身并不必然超时 +- 前端、语义和 IR 也不是根因 +- 真正问题在 Lab3 后端生成的汇编语义 + +进一步缩小后发现: + +- `vector_mul3` 的主循环和点积本身能够结束 +- 真正卡住的是 `my_sqrt` +- 根因是 `my_sqrt` 在输入为 `NaN` 时,循环条件被后端错误判真,导致死循环 + +因此,这不是“性能优化不够”的问题,而是“浮点有序比较语义错误导致的超时型正确性 bug”。 + +修复后,`vector_mul3` 已正常通过。 --- -## 11. 当前测试状态 +## 10. 测试脚本与日志规则 -当前 Lab3 的批量测试脚本为: +Lab3 当前使用的脚本为: -- [scripts/lab3_build_test.sh](/home/hw/nudt-compiler-cpp/scripts/lab3_build_test.sh:1) -- [scripts/verify_asm.sh](/home/hw/nudt-compiler-cpp/scripts/verify_asm.sh:1) +- [scripts/lab3_build_test.sh](../scripts/lab3_build_test.sh) +- [scripts/verify_asm.sh](../scripts/verify_asm.sh) -测试规则为: +测试规则已经固定为: -- 每次运行生成独立目录 `lab3_日期_时间` +- 每次运行生成独立目录 `output/logs/lab3/lab3_YYYYMMDD_HHMMSS/` - 目录中保留完整 `whole.log` -- 成功样例的中间文件自动删除 -- 失败样例保留中间目录与 `error.log` +- 成功样例中间文件自动删除 +- 失败样例保留中间目录 +- 每个失败样例目录必须包含 `error.log` + +也就是说,当前脚本已经符合“只保留失败用例中间文件”的要求。 + +--- + +## 11. 当前测试结果 + +### 11.1 `crypto-*` 修复后的失败集复查 + +在先修完 `phi` lowering 后,失败集复查日志为: + +- [output/logs/lab3/lab3_20260412_143811/whole.log](../output/logs/lab3/lab3_20260412_143811/whole.log) -最新一次失败集复查日志位于: +当时的结果是: -- [output/logs/lab3/lab3_20260412_143811/whole.log](/home/hw/nudt-compiler-cpp/output/logs/lab3/lab3_20260412_143811/whole.log:1) +- `crypto-1.sy` 通过 +- `crypto-2.sy` 通过 +- `crypto-3.sy` 通过 +- `vector_mul3.sy` 仍失败 -当前失败集复查结果为: +这一步证明 `crypto-*` 的根因确实在 `phi` 并行 copy。 -- `PASS test/test_case/h_performance/crypto-1.sy` -- `PASS test/test_case/h_performance/crypto-2.sy` -- `PASS test/test_case/h_performance/crypto-3.sy` -- `FAIL test/class_test_case/performance/vector_mul3.sy` +### 11.2 `vector_mul3` 修复后的单项复查 -也就是说,当前 Lab3 后端已经解决了前期的 `crypto-*` 正确性问题,但仍然保留一个性能型尾项: +只重跑失败集时,日志为: -- `vector_mul3.sy` +- [output/logs/lab3/lab3_20260412_185610/whole.log](../output/logs/lab3/lab3_20260412_185610/whole.log) -该样例当前表现为超时,而不是输出错误。 +结果为: + +- `vector_mul3.sy` 通过 + +这一步证明浮点比较修复已经消除了剩余尾项。 + +### 11.3 最新 Lab3 全量结果 + +最新全量运行日志为: + +- [output/logs/lab3/lab3_20260412_185655/whole.log](../output/logs/lab3/lab3_20260412_185655/whole.log) + +全量结果为: + +- `214 PASS / 0 FAIL / total 214` + +因此,当前 Lab3 后端在现有测试集上已经全部通过。 --- ## 12. 当前结论 -综合来看,当前 Lab3 后端可以准确概括为: +综合来看,当前项目中的 Lab3 后端可以准确概括为: - 已经完成自研 MIR 后端主链路 - 栈布局与调用约定总体符合 `lab03` - 指令选择符合 `lecture05` 的宏扩展与局部模式优化思路,但不是完整树匹配版本 - 寄存器分配高度符合 `lecture11` 的 George 图着色路线 -- `crypto-*` 正确性问题已经修复 -- 仍保留 `vector_mul3` 一个性能型尾项 +- `phi` 并行 copy 正确性问题已经修复 +- 有序浮点比较的 NaN 语义问题已经修复 +- `crypto-*` 和 `vector_mul3` 均已通过 +- 最新 Lab3 全量测试结果为 `214 PASS / 0 FAIL` + +因此,当前更准确的表述已经不是“Lab3 框架基本成型”,而是: + +- Lab3 后端功能链路已经完整 +- 当前测试集下正确性已经收敛 +- 实现风格清晰地对应 `lab03 + lecture05 + lecture11` + +如果后续继续做优化,重点就不再是“修正明显错误”,而是: -因此,当前项目最准确的表述不是“Lab3 完全结束”,而是: +- 提升生成代码质量 +- 扩大可分配寄存器利用范围 +- 增加更强的机器相关优化 -- Lab3 的代码生成框架已经成型并基本可用 -- 正确性主体已经收敛 -- 仍有少量性能尾项待继续优化 +但这些属于后续优化方向,不影响当前 Lab3 已经完成并通过现有测试集这一结论。 diff --git a/src/mir/AsmPrinter.cpp b/src/mir/AsmPrinter.cpp index c835a32..ea07e01 100644 --- a/src/mir/AsmPrinter.cpp +++ b/src/mir/AsmPrinter.cpp @@ -790,11 +790,50 @@ bool TryEmitDirectStore(const MachineFunction& function, const MachineInstr& ins return TryEmitDirectMemoryAccess(function, inst.GetAddress(), type, src.c_str(), true, os); } -const char* GetCondMnemonic(CondCode code) { +const char* GetIntCondMnemonic(CondCode code) { static const char* kCond[] = {"eq", "ne", "lt", "gt", "le", "ge"}; return kCond[static_cast(code)]; } +const char* GetOrderedFloatCondMnemonic(CondCode code) { + switch (code) { + case CondCode::EQ: + return "eq"; + case CondCode::LT: + return "mi"; + case CondCode::GT: + return "gt"; + case CondCode::LE: + return "ls"; + case CondCode::GE: + return "ge"; + case CondCode::NE: + break; + } + throw std::runtime_error(FormatError("mir", "unsupported simple float condition")); +} + +void EmitOrderedFloatBranch(CondCode code, const std::string& true_label, + const std::string& false_label, std::ostream& os) { + if (code == CondCode::NE) { + os << " b.mi " << true_label << "\n"; + os << " b.gt " << true_label << "\n"; + os << " b " << false_label << "\n"; + return; + } + os << " b." << GetOrderedFloatCondMnemonic(code) << " " << true_label << "\n"; + os << " b " << false_label << "\n"; +} + +void EmitOrderedFloatSet(const char* dst, CondCode code, std::ostream& os) { + if (code == CondCode::NE) { + os << " cset w10, mi\n"; + os << " csinc " << dst << ", w10, wzr, le\n"; + return; + } + os << " cset " << dst << ", " << GetOrderedFloatCondMnemonic(code) << "\n"; +} + bool TryEmitFusedCompareBranch(const MachineFunction& function, const MachineInstr& cmp, const MachineInstr& branch, const std::unordered_map& use_counts, @@ -832,9 +871,13 @@ bool TryEmitFusedCompareBranch(const MachineFunction& function, const MachineIns const auto lhs = MaterializeFprUse(function, cmp.GetOperands()[1], 16, 10, os); const auto rhs = MaterializeFprUse(function, cmp.GetOperands()[2], 17, 11, os); os << " fcmp " << lhs << ", " << rhs << "\n"; + EmitOrderedFloatBranch(cmp.GetCondCode(), + BlockLabel(function, branch.GetOperands()[1].GetText()), + BlockLabel(function, branch.GetOperands()[2].GetText()), os); + return true; } - os << " b." << GetCondMnemonic(cmp.GetCondCode()) << " " + os << " b." << GetIntCondMnemonic(cmp.GetCondCode()) << " " << BlockLabel(function, branch.GetOperands()[1].GetText()) << "\n"; os << " b " << BlockLabel(function, branch.GetOperands()[2].GetText()) << "\n"; return true; @@ -1272,8 +1315,7 @@ void EmitFunction(const MachineFunction& function, std::ostream& os) { const auto lhs = MaterializeFprUse(function, inst.GetOperands()[1], 16, 10, os); const auto rhs = MaterializeFprUse(function, inst.GetOperands()[2], 17, 11, os); os << " fcmp " << lhs << ", " << rhs << "\n"; - static const char* kCond[] = {"eq", "ne", "lt", "gt", "le", "ge"}; - os << " cset " << def.reg_name << ", " << kCond[static_cast(inst.GetCondCode())] << "\n"; + EmitOrderedFloatSet(def.reg_name.c_str(), inst.GetCondCode(), os); FinalizeDef(function, vreg, def, os); break; }