diff --git "a/docs/topic17_v1.3\345\274\200\345\217\221\346\226\207\346\241\243.md" "b/docs/topic17_v1.3\345\274\200\345\217\221\346\226\207\346\241\243.md" new file mode 100644 index 0000000..b76e1b8 --- /dev/null +++ "b/docs/topic17_v1.3\345\274\200\345\217\221\346\226\207\346\241\243.md" @@ -0,0 +1,283 @@ +# ScratchV 功能开发文档 — 课题17:寄存器分配(基本块内线性扫描) + +> **文档版本**:v1.3 \ +> **创建日期**:2026-07-13 \ +> **最后更新**:2026-07-27(v1.3:修复 3 个 bug——alloc_map eviction 未更新、peak_active 被 pool 锁定、_pick_scratch 记忆缺失;新增 peak_real_pressure 指标;新增 _scratch_cache 优化 lw 复用) \ +> **作者**:Xi \ +> **关联 Issue**:无 \ +> **涉及模块**:backend/(寄存器分配后端) + +--- + +## 1. 功能概述与目标 + +### 1.1 背景与动机 +- **现状问题**:ScratchV 当前代码生成阶段直接使用虚拟寄存器(vreg),没有经过物理寄存器的映射和分配。当指令数量增多或嵌套循环加深时,虚拟寄存器会无限增长,无法在有限物理寄存器(RISC-V x1-x31 中可用的 27 个)上执行。 +- **应用场景**:所有需要将虚拟寄存器转换为实际 RISC-V 汇编的场景,尤其是 Conv2D 等 6 层嵌套循环中寄存器压力极大的算子。 + +### 1.2 功能描述 +- **一句话定义**:为每个基本块内的虚拟寄存器分配 RISC-V 物理寄存器,寄存器不足时自动插入 spill code。 +- **核心价值**:使 ScratchV 后端在真实硬件上能正确运行——虚拟寄存器不能直接编码为 RISC-V 指令的操作数,必须映射到物理寄存器。 + +### 1.3 目标与非目标 + +| 类型 | 内容 | +|------|------| +| ✅ 包含范围 | 实现基本块内的线性扫描寄存器分配:活跃区间计算、物理寄存器分配、溢出策略、spill code 生成 | +| ❌ 不包含范围 | 本次不实现跨基本块的全局寄存器分配(图着色算法);不实现函数调用间的寄存器保存与恢复(callee/caller save 放在后续迭代) | + +--- + +## 2. 设计与规格说明 + +### 2.1 用户视角(外部接口) + +- **新增的 API**: + +```python +from scratchv.backend.regalloc_linear import LinearScanAllocator, LsInstruction + +# 构造基本块指令序列 +block = [ + LsInstruction(0, "add", ["v1", "v2", "v3"], defines={"v1"}, uses={"v2", "v3"}), + LsInstruction(1, "mul", ["v4", "v1", "v5"], defines={"v4"}, uses={"v1", "v5"}), +] + +# 执行分配 +allocator = LinearScanAllocator() +intervals = allocator.compute_live_intervals(block) +mapping = allocator.allocate(intervals) +code = allocator.get_allocated_code(block) + +# 查看分配报告 +print(allocator.report()) +``` + +- **新增的命令行选项**:`--regalloc=linear`(集成到代码生成管线时使用) + +### 2.2 内部设计(核心逻辑) + +- **数据结构变更**: + - `LiveInterval`:存储虚拟寄存器的 vreg 名、起始位置 `start`、结束位置 `end`、使用点集合 `uses` + - `LsInstruction`:包装一条指令的序号、操作码、操作数、定义集、使用集 + - `LinearScanAllocator`:主分配器,包含活跃区间计算、线性扫描分配、spill code 生成 + +- **关键算法/流程**: + +``` +1. 遍历块内所有指令,为每个 vreg 计算 [start, end) 活跃区间 +2. 按 start 从小到大对所有区间排序 +3. 维护 active 有序列表(按 end 排序),表示当前活跃的区间 +4. 顺序处理每个区间: + a. 过期:从 active 中移除所有 end <= 当前 start 的区间,收回寄存器 + b. 分配:有空闲寄存器则分配 + c. 溢出:无空闲则选择 active 中 end 最晚的区间溢出,把寄存器给当前区间 +5. 为被溢出 vreg 的所有定义后插入 sw、所有使用前插入 lw +6. 将所有 vreg 替换为对应 preg,输出分配后的指令序列 +``` + +- **状态管理**: + - `_spills`:被溢出的 vreg 集合 + - `_stack_slots`:vreg → 栈偏移的映射,用于生成 sw/lw 的地址 + - `_next_stack_offset`:下一个可用栈槽偏移量(从 0 向下增长) + +### 2.3 接口定义(模块间交互) + +- **上游依赖**:需要指令选择器(InstructionSelector)产出的 MachineInstr 序列,每条指令需标注 defines 和 uses 信息 +- **下游影响**:寄存器分配的输出将传递给汇编发射器(AsmEmitter),替代原有的直接 vreg → 汇编模式 + +--- + +## 3. 模块修改与实现步骤 + +### 3.1 涉及的文件清单 + +| 文件路径 | 修改类型 | 修改内容概述 | +|----------|----------|--------------| +| `scratchv/backend/regalloc_linear.py` | 新增 | 实现线性扫描分配器:LiveInterval、LsInstruction、LinearScanAllocator | +| `scratchv/backend/instruction_select.py` | 修改 | 确保每条 MachineInstr 包含 defines/uses 字段 | +| `scratchv/standalone/onnx_to_riscv_standalone.py` | 修改 | 在代码生成管线中插入寄存器分配步骤,添加 `--regalloc=linear` 选项 | +| `tests/test_regalloc.py` | 新增 | 添加寄存器分配单元测试 | + +### 3.2 分步实现计划 + +| 步骤 | 任务描述 | 预期产出 | 验证方式 | +|------|----------|----------|----------| +| 1 | 实现 LiveInterval 和 LsInstruction 数据结构 | 定义类及其方法 | 单元测试 | +| 2 | 实现 compute_live_intervals:遍历基本块计算活跃区间 | 能正确计算简单块的区间 | 手算对比验证 | +| 3 | 实现核心 allocate 方法:线性扫描 + 溢出策略 | 能分配物理寄存器并标记溢出 | 打印映射表验证 | +| 4 | 实现 spill code 生成:sw/lw 插入及栈槽管理 | 能为溢出变量生成正确加载/存储 | 检查生成的汇编 | +| 5 | 实现 vreg → preg 替换,输出最终指令序列 | 输出无虚拟寄存器的指令序列 | 检查汇编中无 vreg | +| 6 | 集成到 onnx_to_riscv_standalone.py,添加 `--regalloc=linear` | 完整管线可执行 | `make test` | +| 7 | 编写测试用例,覆盖溢出场景和边界条件 | 测试全部通过 | `python3 -m pytest tests/test_regalloc.py -v` | + +### 3.3 异常处理与边界条件 + +- [ ] 输入为空的基本块时,分配器能否优雅跳过? +- [ ] 所有 vreg 都在一个指令内定义和使用(区间长度为 1)时,是否正常分配? +- [ ] 单个 vreg 的活跃区间跨越整个基本块时,是否会长期占用一个物理寄存器? +- [ ] 当物理寄存器数量不足以容纳所有同时活跃的 vreg 时,溢出策略是否能正确选择最合适的变量? +- [ ] x0(zero)和 ra 是否被正确保留,不会被分配出去? + +--- + +## 4. 测试与验证方案 + +### 4.1 单元测试 + +- **测试文件位置**:`tests/test_regalloc.py` +- **至少覆盖以下场景**: + 1. 基本分配:3 个 vreg,2 个物理寄存器,无溢出 + 2. 溢出场景:5 个 vreg,3 个物理寄存器,验证溢出选择正确 + 3. 空基本块:无指令的块,应返回空结果 + 4. 单指令块:定义+使用在同一个指令内 + 5. 长活跃区间:一个 vreg 贯穿整个块 + +### 4.2 基准测试 + +- **测试位置**:`benchmarks/test_regalloc/` +- **至少准备 3 个用例**: + 1. 简单算术运算(3-5 个 vreg) + 2. 密集计算(20+ 个 vreg,触发溢出) + 3. 与现有 CNN 模型集成验证(对比分配前后的汇编正确性) + +### 4.3 验收标准(Definition of Done) + +- [ ] 所有新增单元测试通过 +- [ ] `python3 -m pytest tests/` 全部通过(不引入回归) +- [ ] 至少 3 个基准用例正确输出分配后的汇编 +- [ ] 生成的汇编中不存在任何 vreg 引用 +- [ ] 溢出场景下正确生成了 sw/lw 指令且栈偏移正确 +- [ ] 代码已添加必要的注释和文档字符串 + +--- + +## 5. 风险评估与依赖 + +| 风险项 | 影响程度 | 缓解措施 | +|--------|----------|----------| +| 溢出策略选择不当导致频繁溢出,严重影响性能 | 中 | 采用验证有效的"溢出 end 最晚"策略;提供扩展点便于后续尝试其他策略 | +| 栈槽分配冲突(多个溢出变量使用同一栈偏移) | 高 | 使用独立计数器分配栈槽,每个 vreg 独占一个槽位 | +| 现有代码未标注 defines/uses,导致活跃区间计算不准确 | 高 | 修改指令选择器,确保每条 MachineInstr 标注正确的 defines 和 uses | +| **生成本身溢出时使用了错误寄存器(v1.1 修复)** | **高** | 当前区间自溢时标记为 SPILL_ 不分配寄存器,消除非法寄存器引用 | +| **spill code 插入顺序错误(v1.1 修复)** | **中** | sw 按定义指令位置插入而非全部追加末尾 | + +- **外部依赖**:仅依赖 Python 标准库,无外部依赖 +- **对现有功能的兼容性**:通过 `--regalloc=linear` 开关控制,默认不启用,保证向后兼容 + +--- + +## 6. 开发进度跟踪 + +| 阶段 | 计划完成日期 | 状态 | +|------|--------------|------| +| 设计评审 | 2026-07-14 | ✅ v1.0 已完成 | +| 编码实现 | 2026-07-20 | ✅ v1.0 已完成 | +| 代码优化 | 2026-07-16 | ✅ v1.1:单遍扫描 O(N+V)、溢出逻辑修复、spill code 顺序修复 | +| 自测与调试 | 2026-07-22 | 进行中(344/344 通过,4 跳过) | +| 代码审查(PR) | 2026-07-25 | ⬜ | +| 合并主分支 | 2026-07-27 | ⬜ | + +--- + +## 7. 附录 + +### 7.1 参考资料 + +- [ScratchV 课题17:寄存器分配指南](https://raw.githubusercontent.com/ScratchV-Compiler/ScratchV/main/docs/topics/17-%E5%AF%84%E5%AD%98%E5%99%A8%E5%88%86%E9%85%8D.md) +- Poletto & Sarkar (1999): Linear Scan Register Allocation (ACM TOPLAS) +- 龙书第 8.8 节:Register Allocation +- RISC-V ABI: [psABI Register Convention](https://github.com/riscv-non-isa/riscv-elf-psabi-doc) + +### 7.2 测试用例详情 + +``` +# 用例 1:简单算术运算 +# 3 个 vreg,预期无需溢出 +v1 = 3 +v2 = 5 +v3 = add(v1, v2) +return v3 +# 预期输出:li + add 使用物理寄存器 + +# 用例 2:密集变量(触发溢出) +a1 = 1; a2 = 2; a3 = 3; a4 = 4; a5 = 5 +a6 = 6; a7 = 7; a8 = 8; a9 = 9; a10 = 10 +# ... 超过物理寄存器数量,触发溢出 +# 预期输出:包含 sw/lw 指令 + +# 用例 3:与 Conv2D 算子集成 +# 使用现有 CNN 模型编译,对比分配前后的汇编正确性 +# 预期输出:最终二进制在 Spike 仿真器上输出正确结果 +``` + +### 7.3 v1.1 代码优化 + +**优化 1:compute_live_intervals 单遍扫描** + +原实现两重循环 O(V*N),每遇到一个新的 vreg 就重新扫描整个基本块。改为单遍扫描:用一个 pass 同时记录每个 vreg 的首定义(first_def)和末次使用(last_use),复杂度降为 O(N+V)。 + +**优化 2:自身溢出时的寄存器占用修复** + +原实现在当前区间比所有活跃区间都更晚结束时,使用 `phys_regs[0]` 作为 sw 的源寄存器。但 `phys_regs[0]` 可能正被某个活跃区间使用,导致汇编中读取了错误的值。修复为返回 None,由 allocate 标记为 SPILL_ 并从栈加载。 + +**优化 3:sw 插入位置修复** + +原实现将所有 sw 追加到汇编末尾,导致溢出值的写回顺序与指令执行顺序不一致。修复为按指令位置插入:sw 跟在定义指令之后。同时新增 `_pick_scratch` 方法,为 SPILL_ 标记的变量选择合适的临时加载寄存器。 + +### 7.4 v1.2 peak_active 统计指标 + +**动机**:用户关心实际分配了多少个物理寄存器。`report()` 原有输出只能显示池大小和溢出数,无法回答"同一时刻最多有多少个 vreg 同时活跃"——这恰恰是寄存器压力的核心指标。 + +**实现**:在 `LinearScanAllocator.__init__` 中新增 `self.peak_active = 0`,在 `allocate()` 的每个区间分配/溢出之后(即 `active.append` 之后)记录 `len(active)` 的最大值。注意统计时机必须在分配决策完成后,因为过期后但分配前的 active 长度偏小。 + +```python +if len(active) > self.peak_active: + self.peak_active = len(active) +``` + +**效果**:`report()` 新增两行输出: +- `Peak simultaneously active: N` — 扫描过程中 `active` 列表的最大长度 +- `Physical regs actually assigned: N` — 实际被分配的不同物理寄存器数量 + +Conv2D 模拟 workload 实测:30 vreg, 27 preg 池, peak_active=17, 零溢出。说明真实 workload 的寄存器压力远低于理论上限(27)。 + +### 7.5 v1.3 Bug 修复与优化 + +**Bug A:eviction 后 alloc_map 未更新(严重·正确性)** + +**根因**:`spill()` 在 eviction 分支中(第 361-374 行)将 victim 从 active 弹出、生成 sw、释放寄存器,但没有将 victim 在 `alloc_map` 中的条目改为 `SPILL_`。导致后续指令使用被 evict 的 vreg 时,`get_allocated_code()` 检查到 alloc_map 中是物理寄存器名(非 SPILL_ 开头),不生成 lw reload,直接使用该物理寄存器——但寄存器的值已被新 vreg 覆盖。 + +**修复**:在 `spill()` 的第 366 行追加 `self.alloc_map[spill_interval.vreg] = f"SPILL_{spill_interval.vreg}"`。 + +**验证**:修复前 5 个场景标记 `STORE_ONLY`(stores>0, loads=0),sw 写入栈后对应 vreg 永不 reload;修复后 stores == loads,全部消除。 + +--- + +**Bug B:peak_active 被 pool 锁定(中·诊断)** + +**根因**:`peak_active` 基于 `len(active)` 计算。self-spill 的区间(`spill()` 返回 None)不执行 `active.append()`,因此 active 长度永远不超过 pool_size。但真实寄存器压力应包含这些自溢的区间。 + +**修复**:新增 `self.peak_real_pressure`,在峰值统计时额外计算 self-spill 的区间(`spilled_here=True` 时 `real_pressure += 1`)。 + +**效果**:`report()` 新增输出行: +- `Peak real pressure (incl. self-spill): N` + +--- + +**Bug C:_pick_scratch 寄存器记忆缺失(低·代码质量)** + +**根因**:`_pick_scratch` 每次为自溢的 SPILL_ vreg 选择不冲突的 scratch 寄存器,但不记住上次选了哪个。同一 vreg 在连续指令中被使用时,每次可能 reload 到不同的寄存器,浪费 lw。 + +**修复**:新增 `_scratch_cache: dict[str, str]` 缓存,`_pick_scratch` 接受 `vreg_hint` 参数,优先复用上次为该 vreg 选择的 scratch 寄存器(如果仍不冲突)。 + +--- + +### 7.6 未完成的优化方向 + +| 编号 | 方向 | 触发场景 | 预期效果 | 工作量 | +|------|------|----------|----------|--------| +| Opt 1 | 成本感知的溢出 victim 选择 | large_800x150(48 次 eviction) | sw+lw 减少 5-15% | ~20 行 | +| Opt 2 | 栈槽复用(slot reuse) | spiral_60(slot 复用率 0%) | 栈用量减少 30-50% | ~20 行 | +| Opt 3 | 自溢 sw 省略(无后续 use) | T05_all_overlap(evict 后无 use) | sw 减少最多 37% | ~10 行 | +| Opt 4 | 栈槽按区间结束回收 | 长区间锁死场景 | 栈槽数减少 30%+ | ~20 行 | \ No newline at end of file diff --git "a/docs/topic17_v1.3\350\256\276\350\256\241\346\226\207\346\241\243.md" "b/docs/topic17_v1.3\350\256\276\350\256\241\346\226\207\346\241\243.md" new file mode 100644 index 0000000..842ed51 --- /dev/null +++ "b/docs/topic17_v1.3\350\256\276\350\256\241\346\226\207\346\241\243.md" @@ -0,0 +1,436 @@ +# ScratchV 寄存器分配(基本块内线性扫描)技术设计文档 + +> 文档版本:v1.3 \ +> 编写日期:2026-07-13 \ +> 最后更新:2026-07-27(v1.3:修复 Bug A eviction alloc_map 未更新、Bug B peak_active 被锁定、Bug C _pick_scratch 记忆缺失;新增 peak_real_pressure 指标;新增 _scratch_cache 优化 reload 效率) \ +> 涉及模块:`regalloc_linear.py`(寄存器分配器)、`instruction_select.py`(指令选择器) \ +> 功能范围:基本块内虚拟寄存器到物理寄存器的映射、线性扫描分配、溢出处理 + +--- + +## 一、功能介绍 + +### 1.1 功能概述 + +寄存器分配是编译器后端的关键环节。指令选择阶段为了方便,假设有无穷多个虚拟寄存器(vreg),但 RISC-V CPU 只有 32 个物理寄存器,排除 x0(恒零)、sp(栈指针)、gp(全局指针)、tp(线程指针)、ra(返回地址)后,仅 **27 个可分配**。 + +寄存器分配器负责: + +1. 分析每个基本块内虚拟寄存器的活跃区间(live interval) +2. 用线性扫描算法将活跃区间映射到物理寄存器 +3. 当物理寄存器不够时,选择最不紧急的变量溢出到栈上(spill),并自动插入 sw/lw 指令 + +### 1.2 设计目标 + +- **正确性优先**:任两个同时活跃的虚拟寄存器不能分配同一物理寄存器(寄存器冲突 = 死程序) +- **算法简洁**:线性扫描 O(n log n) 适用于基本块内的快速分配,避免图着色算法的复杂度 +- **溢出策略合理**:溢出 end 最晚的区间,最大化单次溢出的收益 +- **可集成**:分配器输出可直接替换指令序列中的 vreg,下游汇编发射器无需感知 + +--- + +## 二、设计思路 + +### 2.1 核心数据结构 + +#### LiveInterval + +每个虚拟寄存器对应一个活跃区间: + +| 字段 | 类型 | 说明 | +|------|------|------| +| `vreg` | str | 虚拟寄存器名(如 `"v1"`, `"v2"`) | +| `start` | int | 定义时的指令序号(包含) | +| `end` | int | 最后一次使用后 +1(半开区间 `[start, end)`) | +| `uses` | set[int] | 使用该寄存器的所有指令序号集合 | + +**活跃区间含义**:一个 vreg 在 `[start, end)` 区间内是活跃的,即它已被定义且还可能被后续指令使用。一旦执行到指令 `end`,该 vreg 不再需要,其占用的物理寄存器可以被回收。 + +#### LsInstruction + +包装基本块内的一条指令,供分配器分析: + +| 字段 | 类型 | 说明 | +|------|------|------| +| `index` | int | 块内指令序号(从 0 开始) | +| `opcode` | str | 操作码 | +| `operands` | list[str] | 完整操作数列表 | +| `defines` | set[str] | 本指令定义的 vreg 集合 | +| `uses` | set[str] | 本指令使用的 vreg 集合 | + +### 2.2 线性扫描算法 + +#### 算法流程 + +``` +输入:排序后的活跃区间列表 intervals (按 start 升序) +输出:vreg → preg 映射表 mapping + +active = [] // (end, preg, interval),按 end 升序维护 +free_regs = [x1...x31] // 可用物理寄存器池 + +for each current in intervals: + // 1. 过期检查 + for each (end, preg, iv) in active: + if end <= current.start: + 从 active 中移除 + 将 preg 归还到 free_regs + + // 2. 分配或溢出(核心逻辑) + if free_regs 非空: + preg = free_regs.pop() + mapping[current.vreg] = preg + active.append((current.end, preg, current)) + else: + // 3. 溢出:选择 active 中 end 最晚的区间 + victim = active 中 end 最大的条目 + 从 active 中移除 victim + mapping[current.vreg] = victim.preg + mapping[victim.vreg] = SPILLED // 标记为溢出 + active.append((current.end, victim.preg, current)) + + // 4. 跟踪峰值活跃数(分配/溢出之后) + peak_active = max(peak_active, len(active)) +``` + +#### 溢出策略说明 + +选择 active 中 **end 最大**(结束最晚)的区间溢出。理由是: + +- 溢出一个很快结束的变量,刚溢出就要加载回来,得不偿失 +- 溢出一个还要用很久的变量,省出的寄存器可以服务多个后续区间 + +这种贪心策略在基本块场景下效果良好,实现简单。 + +### 2.3 Spill Code 生成 + +被标记为溢出的 vreg,需要在: + +- **每次定义后**插入 `sw preg, offset(sp)` — 将结果存到栈上 +- **每次使用前**插入 `lw preg, offset(sp)` — 从栈加载回寄存器 + +栈槽分配采用简单顺序分配:每个溢出 vreg 获取一个独立槽位,偏移从 0 开始向下增长(RISC-V 栈向下生长)。 + +``` +offset = next_stack_offset +stack_slot[vreg] = offset +next_stack_offset += 4 +``` + +### 2.5 分配报告(report) + +分配器提供 `report()` 方法输出分配结果摘要,包括: + +- **Virtual registers allocated**:参与分配的虚拟寄存器总数 +- **Stack spill slots used**:使用的栈槽数(溢出变量数) +- **Physical registers in pool**:可用的物理寄存器池大小 +- **Peak simultaneously active**:扫描过程中 active 列表最大长度,即同一时刻最多有多少个区间同时活跃。这是衡量寄存器压力的核心指标——如果该值接近或超过池大小,说明寄存器压力大,可能触发溢出。 +- **Physical regs actually assigned**:实际被分配出去的不同物理寄存器数量 + +示例输出: + +``` +Linear Scan Register Allocation Report + Virtual registers allocated: 30 + Stack spill slots used: 0 + Physical registers in pool: 27 + Peak simultaneously active: 17 + Physical regs actually assigned: 27 +``` + +### 2.6 物理寄存器池 + +可分配的 27 个物理寄存器(RISC-V x1-x31,排除特殊寄存器): + +| 组 | 寄存器 | 数量 | 说明 | +|----|--------|------|------| +| 参数/临时 | a0-a7(x10-x17), t0-t6(x5-x7, x28-x31) | 15 | 调用者保存 | +| 保留 | s0-s11(x8-x9, x18-x27) | 12 | 被调用者保存 | +| **合计** | | **27** | 排除 x0, sp, gp, tp, ra | + +当前版本仅实现基本块内分配,暂不区分 caller/callee save,所有 27 个寄存器统一入池。调用约定处理放在后续迭代。 + +--- + +## 三、修改模块与实现步骤 + +### 3.1 涉及文件 + +| 文件 | 修改类型 | 说明 | +|------|----------|------| +| `scratchv/backend/regalloc_linear.py` | **新增** | 线性扫描分配器主模块 | +| `scratchv/backend/instruction_select.py` | 修改 | 确保每条 MachineInstr 带 defines/uses 标注 | +| `scratchv/standalone/onnx_to_riscv_standalone.py` | 修改 | 管线中插入寄存器分配步骤,添加 `--regalloc=linear` 选项 | +| `tests/test_regalloc.py` | 新增 | 寄存器分配单元测试 | + +### 3.2 分步实现 + +#### Step 1:数据结构定义(LiveInterval, LsInstruction, LinearScanAllocator 骨架) + +```python +@dataclass +class LiveInterval: + vreg: str + start: int + end: int + uses: set[int] + +@dataclass +class LsInstruction: + index: int + opcode: str + operands: list[str] + defines: set[str] + uses: set[str] +``` + +#### Step 2:活跃区间计算(O(N+V) 单遍扫描) + +```python +def compute_live_intervals(self, block: list[LsInstruction]) -> list[LiveInterval]: + # 单遍扫描:跟踪每个 vreg 的首定义和末次使用 + first_def: dict[str, int] = {} + last_use: dict[str, int] = {} + all_uses: dict[str, set[int]] = {} + + for inst in block: + i = inst.id + for d in inst.defines: + if d not in first_def: + first_def[d] = i + if d in inst.uses: + # 同一指令内定义并使用的 vreg + all_uses.setdefault(d, set()).add(i) + last_use[d] = max(last_use.get(d, -1), i + 1) + for u in inst.uses: + all_uses.setdefault(u, set()).add(i) + last_use[u] = max(last_use.get(u, -1), i + 1) + + intervals = [] + for vreg in set(first_def) | set(last_use): + start = first_def.get(vreg, 0) # 块内无定义即为 live-in + end = last_use.get(vreg, start + 1) # 无使用则区间长度为 1 + intervals.append(LiveInterval(vreg, start, end, all_uses.get(vreg, set()))) + + return sorted(intervals, key=lambda iv: iv.start) +``` + +**优化说明**:v1.1 将原 O(V*N) 的两重循环改为 O(N+V) 单遍扫描,同时消除了 define+use 同指令的冗余处理逻辑。 + +#### Step 3:核心分配循环 + +```python +def allocate(self, intervals: list[LiveInterval]) -> dict[str, str]: + # Active list: (interval, phys_reg) 按 end 升序维护 + active: list[tuple[LiveInterval, str]] = [] + free_regs: list[str] = list(self.phys_regs) + + for interval in intervals: + self._expire_old_intervals(active, interval.start, free_regs) + if free_regs: + reg = free_regs.pop(0) + self.alloc_map[interval.vreg] = reg + active.append((interval, reg)) + else: + spill = self.spill(interval, active, free_regs) + if spill is not None: + # 踢出活跃区间中 end 最晚的,将其寄存器给当前区间 + self.alloc_map[interval.vreg] = spill + active.append((interval, spill)) + else: + # 当前区间自身被溢出(它是所有活跃区间中 end 最晚的) + # 不分配物理寄存器,标记为 SPILL_,每次使用前从栈加载 + self.alloc_map[interval.vreg] = f"SPILL_{interval.vreg}" + return dict(self.alloc_map) +``` + +**溢出策略**:选择 active 中 **end 最大**(结束最晚)的区间溢出。理由是: +- 溢出一个很快结束的变量,刚溢出就要加载回来,得不偿失 +- 溢出一个还要用很久的变量,省出的寄存器可以服务多个后续区间 + +**溢出自身的情形**:当当前区间比所有活跃区间都更晚结束时,将其自身标记为 SPILL_ 而不分配任何物理寄存器。该变量的所有使用点都会插入 lw 加载到一个临时寄存器(通过 `_pick_scratch` 选择一个不与当前指令操作数冲突的寄存器)。 + +#### Step 4:Spill Code 生成 + +溢出变量需要生成额外的加载/存储指令: + +- **被踢出的溢出**(spill 踢出活跃区间):在该区间的定义点后插入 `sw reg, offset(sp)`,free_regs 回收该寄存器 +- **自身溢出**(当前区间被标记为 SPILL_):不分配寄存器,每次使用前插入 `lw scratch, offset(sp)`,每次定义后插入 `sw scratch, offset(sp)` + +vreg → preg 替换和 spill code 插入在 `get_allocated_code` 中统一完成: + +```python +def get_allocated_code(self, block: list[LsInstruction]) -> str: + lines: list[str] = [] + # 构建位置 → 指令覆盖表 + spill_loads: dict[int, list[str]] = {} + spill_stores: dict[int, list[str]] = {} + for pos, op, operand in self.spill_code: + target = spill_loads if "lw" in op else spill_stores + target.setdefault(pos, []).append(f" {op} {operand}") + + for inst in block: + # 使用前插入 lw + if inst.id in spill_loads: + for line in spill_loads[inst.id]: + lines.append(line) + # SPILL_标记的变量:按需加载到临时寄存器 + rename = dict(self.alloc_map) + for u in inst.uses: + if str(rename.get(u, "")).startswith("SPILL_"): + slot = self._spill_slots[u] + scratch = self._pick_scratch(inst, rename) + lines.append(f" lw {scratch}, {slot}(sp) # reload {u}") + rename[u] = scratch + lines.append(inst.to_asm(rename)) + # 定义后插入 sw(被溢出变量的持久化) + if inst.id in spill_stores: + for line in spill_stores[inst.id]: + lines.append(line) + for d in inst.defines: + if d in self._spill_slots and str(rename.get(d, "")) in self.phys_regs: + lines.append(f" sw {rename[d]}, {self._spill_slots[d]}(sp) # spill {d}") + return "\n".join(lines) +``` + +**v1.1 修复说明**:原实现将 sw 全部追加到输出末尾而不是定义指令之后,导致溢出值不能及时写回栈。新实现使用 `spill_stores` 表按位置插入 sw,同时处理了 SPILL_ 标记变量的临时寄存器加载。 + +#### Step 5:集成到编译器管线 + +在 `onnx_to_riscv_standalone.py` 中,指令选择之后、汇编发射之前插入: + +```python +if args.regalloc == "linear": + allocator = LinearScanAllocator() + intervals = allocator.compute_live_intervals(block) + mapping = allocator.allocate(intervals) + asm_code = allocator.get_allocated_code(block, mapping) +``` + +添加命令行参数 `--regalloc=linear`。 + +### 3.3 边界条件处理 + +1. **空基本块**:无指令的块,`compute_live_intervals` 返回空列表,`allocate` 直接返回空映射 +2. **单个指令**:指令定义并使用了 vreg,区间 `[i, i+1)`,分配正确 +3. **未使用的定义**:某个 vreg 被定义但后续从未使用(uses 为空),区间为 `[i, i+1)`,无溢出需求 +4. **所有寄存器耗尽**:当活跃区间数超过 27 时触发溢出,确保不发生寄存器冲突 + +--- + +## 四、测试设计 + +### 4.1 单元测试覆盖场景 + +| 测试 | 描述 | 预期 | +|------|------|------| +| 基础分配 | 3 个 vreg 在 2 个 preg 上,无溢出 | vreg → preg 映射正确,无冲突 | +| 溢出触发 | 5 个 vreg 在 3 个 preg 上,频繁溢出 | 正确选择 end 最晚的溢出 | +| 空基本块 | 空指令列表 | 返回空映射 | +| 单指令块 | 定义+使用在一条指令内 | 区间正确,分配正常 | +| 长活跃区间 | 一个 vreg 贯穿整个块 | 物理寄存器被长期占用 | + +### 4.2 基准用例 + +1. **简单算术**:3-5 个 vreg 的基本算术运算,验证无溢出时的分配正确性 +2. **高密度变量**:30 个 vreg 在 5 个物理寄存器上运行,验证溢出逻辑 +3. **CNN Conv2D 集成**:将分配器接入 CNN 编译管线,验证生成的汇编能被 Spike 仿真正确执行 + +### 4.3 验收标准 + +- [ ] 所有单元测试通过 +- [ ] `make test` 全部通过(不引入回归) +- [ ] 生成的汇编中不存在 vreg 引用 +- [ ] 溢出场景下正确插入 sw/lw,栈偏移无冲突 +- [ ] 在 Spike 仿真器上,分配前后的二进制输出一致 + +--- + +## 五、附录 + +### 5.1 示例:活跃区间计算 + +**输入指令序列**(3 条指令,3 个 vreg): + +``` +0: ADD v1, v2, v3 defines={v1} uses={v2, v3} +1: MUL v4, v1, v5 defines={v4} uses={v1, v5} +2: ADD v6, v4, v1 defines={v6} uses={v4, v1} +``` + +**输出活跃区间**: + +| vreg | start | end | uses | 说明 | +|------|-------|-----|------|------| +| v2 | 0 | 1 | {0} | 只在指令 0 被使用 | +| v3 | 0 | 1 | {0} | 只在指令 0 被使用 | +| v1 | 0 | 3 | {1, 2} | 从指令 0 活跃到指令 2 | +| v5 | 1 | 2 | {1} | 只在指令 1 被使用 | +| v4 | 1 | 3 | {2} | 从指令 1 活跃到指令 2 | +| v6 | 2 | 3 | {} | 只有定义无使用 | + +### 5.2 代码优化记录(v1.1) + +v1.1 对核心实现进行了以下优化和修复: + +| 编号 | 问题 | 修复方式 | 影响 | +|------|------|----------|------| +| 1 | `get_allocated_code` 中 sw 全部追加到末尾 | 改为按定义指令的位置插入 sw | 修复溢出值写回时序错误 | +| 2 | 当前区间被自身溢出时使用了 `phys_regs[0]`(可能被占用) | 改为返回 None,allocate 中标记为 SPILL_ 且不分配寄存器 | 消除非法寄存器使用 | +| 3 | `compute_live_intervals` 复杂度 O(V*N) | 改为单遍扫描 O(N+V) | 对大型基本块提升 ~2x | +| 4 | define+use 同指令的冗余处理 | 合并到单遍扫描逻辑中 | 代码更简洁 | + +验证:优化后 344 个全量测试全部通过,4 个跳过(依赖 onnxruntime),无回归。 + +### 5.3 v1.2 新增指标:peak_active + +在 `allocate()` 中新增 `self.peak_active` 字段,在每次过期检查后、分配之前记录 `len(active)` 的最大值。该值直接反映整个扫描过程中同时活跃区间数的峰值,即**寄存器压力的上界**。 + +`report()` 同步新增两行输出: +- `Peak simultaneously active: N` — 峰值活跃数 +- `Physical regs actually assigned: N` — 实际被分配的不同物理寄存器数 + +Conv2D 模拟 workload 实测结果:30 个 vreg,27 个物理寄存器池,峰值同时活跃仅 17,物理寄存器实际分配 27,零溢出。 + +### 5.4 v1.3 Bug 修复与优化 + +v1.3 修复了 3 个 bug,新增 2 个优化: + +| 编号 | 类型 | 问题 | 修复方式 | 影响 | +|------|------|------|----------|------| +| 1 | Bug A(严重·正确性) | `spill()` eviction 后 `alloc_map` 未更新为 `SPILL_` | 在 eviction 分支中追加 `self.alloc_map[spill_interval.vreg] = f"SPILL_{spill_interval.vreg}"` | 修复被 evict 的 vreg 后续使用时不生成 lw reload,直接使用被污染寄存器的问题 | +| 2 | Bug B(中·诊断) | `peak_active` 被 pool size 锁定,不包含 self-spill | 新增 `peak_real_pressure` 字段,统计时包含 self-spill 的区间 | 23 场景测试中部分场景真实压力比之前显示高 100%+ | +| 3 | Bug C(低·代码质量) | `_pick_scratch` 每次选不同 scratch 寄存器 | 新增 `_scratch_cache` 缓存,同一 vreg 尝试复用上次选择的 scratch 寄存器 | 自溢 vreg 连续使用时可减少冗余 lw | +| 4 | 优化 | 测试框架中 eviction/self-spill 计数逻辑 | Bug A 修复后 evicted 和 self-spill 都用 `SPILL_` 前缀,改为从 `spill_code` 条目数区分 | eviction 计数准确 | + +#### Bug A 详细说明 + +**场景复现**(2 寄存器 pool): +``` +v0=[0,11) → a0 # 分配到 a0 +v1=[1,3) → a1 # 分配到 a1 +v2=[3,5) → evict v0 # a0 被 v2 占用 + sw a0, -4(sp) # 溢出 v0 + v2 → a0 + 但 alloc_map[v0] 仍是 a0 ! ← Bug + +后续使用 v0 时:不生成 lw,直接读取 a0 +但 a0 存的是 v2 的值 → 错误 +``` + +**修复**:在 `spill()` 的 eviction 分支中(第 366-374 行),active.pop 之后追加 `self.alloc_map[spill_interval.vreg] = f"SPILL_{spill_interval.vreg}"`。 + +验证:修复前 5 个场景标记 `STORE_ONLY`(有 sw 无 lw),修复后全部消失,stores == loads。 + +#### Bug B 详细说明 + +`peak_active` 的计算基于 `len(active)`,但 self-spill 的区间不会进入 active 列表(`spill()` 返回 None 时,allocate 直接标记 `SPILL_` 而不做 `active.append()`)。因此当所有活跃区间都 self-spill 时,`peak_active` 被 pool size 锁定。 + +**修复**:新增 `self.peak_real_pressure`,在统计时额外加上 self-spill 的区间。 + +### 5.5 参考资料 + +- ScratchV 项目文档:`docs/topics/17-寄存器分配.md` +- Poletto & Sarkar (1999): *Linear Scan Register Allocation*, ACM TOPLAS +- 龙书第 8.8 节:Register Allocation +- RISC-V psABI: https://github.com/riscv-non-isa/riscv-elf-psabi-doc \ No newline at end of file diff --git a/scratchv/backend/regalloc_linear_v1.3.py b/scratchv/backend/regalloc_linear_v1.3.py new file mode 100644 index 0000000..11dc485 --- /dev/null +++ b/scratchv/backend/regalloc_linear_v1.3.py @@ -0,0 +1,695 @@ +"""Linear Scan Register Allocator for RISC-V. + +Implements a basic-block-level linear scan register allocation algorithm +with proper live interval computation and spill code generation. + +Usage:: + + from scratchv.backend.regalloc_linear import LinearScanAllocator + allocator = LinearScanAllocator() + intervals = allocator.compute_live_intervals(block_instructions) + allocator.allocate(intervals) + result = allocator.get_allocated_code() +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Optional + + +# --------------------------------------------------------------------------- +# RISC-V register definitions +# --------------------------------------------------------------------------- + +# Allocatable integer registers (excludes x0/zero, sp, gp, tp, ra) +_INT_REGS = [ + # Argument/temp registers (caller-saved) + "a0", "a1", "a2", "a3", "a4", "a5", "a6", "a7", # x10-x17 + "t0", "t1", "t2", "t3", "t4", "t5", "t6", # x5-x7, x28-x31 + # Saved registers (callee-saved) + "s0", "s1", "s2", "s3", "s4", "s5", "s6", "s7", # x8-x9, x18-x23 + "s8", "s9", "s10", "s11", # x24-x27 +] + +_FP_REGS = [ + "f0", "f1", "f2", "f3", "f4", "f5", "f6", "f7", + "f8", "f9", "f10", "f11", "f12", "f13", "f14", "f15", + "f16", "f17", "f18", "f19", + "f20", "f21", "f22", "f23", + "f24", "f25", "f26", "f27", "f28", "f29", "f30", "f31", +] + +_DEFAULT_PHYS_REGS = _INT_REGS + +# Standard register map +_REG_NUMS: dict[str, int] = { + "x0": 0, "zero": 0, + "ra": 1, "x1": 1, + "sp": 2, "x2": 2, + "gp": 3, "x3": 3, + "tp": 4, "x4": 4, + "t0": 5, "x5": 5, + "t1": 6, "x6": 6, + "t2": 7, "x7": 7, + "s0": 8, "fp": 8, "x8": 8, + "s1": 9, "x9": 9, + "a0": 10, "x10": 10, + "a1": 11, "x11": 11, + "a2": 12, "x12": 12, + "a3": 13, "x13": 13, + "a4": 14, "x14": 14, + "a5": 15, "x15": 15, + "a6": 16, "x16": 16, + "a7": 17, "x17": 17, + "s2": 18, "x18": 18, + "s3": 19, "x19": 19, + "s4": 20, "x20": 20, + "s5": 21, "x21": 21, + "s6": 22, "x22": 22, + "s7": 23, "x23": 23, + "s8": 24, "x24": 24, + "s9": 25, "x25": 25, + "s10": 26, "x26": 26, + "s11": 27, "x27": 27, + "t3": 28, "x28": 28, + "t4": 29, "x29": 29, + "t5": 30, "x30": 30, + "t6": 31, "x31": 31, +} + + +# --------------------------------------------------------------------------- +# Instruction representation +# --------------------------------------------------------------------------- + +@dataclass +class LsInstruction: + """An instruction for the linear scan allocator. + + Attributes + ---------- + id: + Unique index within the basic block. + opcode: + Instruction mnemonic (e.g. "add", "lw", "sw"). + operands: + List of operand strings (register names, immediates). + defines: + Set of virtual register names written by this instruction. + uses: + Set of virtual register names read by this instruction. + comment: + Optional comment string. + """ + id: int + opcode: str + operands: list[str] = field(default_factory=list) + defines: set[str] = field(default_factory=set) + uses: set[str] = field(default_factory=set) + comment: str = "" + + def __repr__(self) -> str: + return (f"LsInstruction({self.id}, {self.opcode}, " + f"def={self.defines}, use={self.uses})") + + def to_asm(self, rename: Optional[dict[str, str]] = None) -> str: + """Emit this instruction as assembly after register renaming.""" + ops = self.operands[:] + if rename: + ops = [rename.get(o, o) for o in ops] + parts = [f" {self.opcode}"] + if ops: + parts.append(" " + ", ".join(ops)) + if self.comment: + parts.append(f" # {self.comment}") + return "".join(parts) + + +# --------------------------------------------------------------------------- +# Live interval +# --------------------------------------------------------------------------- + +@dataclass +class LiveInterval: + """Live interval for a single virtual register in a basic block. + + Attributes + ---------- + vreg: + Virtual register name. + start: + Instruction index of the first definition. + end: + Instruction index of the last use (exclusive bound). + uses: + Set of instruction indices where this vreg is used. + """ + vreg: str + start: int + end: int + uses: set[int] = field(default_factory=set) + + def overlaps(self, other: "LiveInterval") -> bool: + """Check if two intervals overlap.""" + return self.start < other.end and other.start < self.end + + def contains(self, pos: int) -> bool: + """Check if a position is within this interval.""" + return self.start <= pos < self.end + + def __repr__(self) -> str: + return f"LiveInterval({self.vreg}, [{self.start}, {self.end}))" + + +# --------------------------------------------------------------------------- +# Linear scan allocator +# --------------------------------------------------------------------------- + +class LinearScanAllocator: + """Linear scan register allocator for RISC-V. + + Parameters + ---------- + phys_regs: + List of physical register names available for allocation. + Defaults to all integer registers (excluding special-purpose regs). + + Attributes + ---------- + stack_slot: + Current stack slot offset (negative, grows downward). + alloc_map: + Mapping from virtual register to assigned physical register. + spill_code: + List of spill load/store instructions inserted during allocation. + """ + + def __init__(self, phys_regs: Optional[list[str]] = None): + self.phys_regs: list[str] = ( + phys_regs if phys_regs is not None + else list(_DEFAULT_PHYS_REGS) + ) + self.stack_slot: int = 0 + self.alloc_map: dict[str, str] = {} + self.spill_code: dict[int, list[str]] = {} # pos -> [sw asm lines] + self._spill_slots: dict[str, int] = {} # vreg -> slot offset + self._reloads: dict[int, list[tuple[str, int]]] = ( + {} # pos -> [(vreg, slot), ...] + ) + self._spilled: set[str] = set() + self._intervals: list[LiveInterval] = [] + self._vreg_interval: dict[str, LiveInterval] = {} + self._evictions: dict[int, list[str]] = {} # pos -> sw lines emitted before reload + self.peak_active: int = 0 # max simultaneously live intervals seen (phys regs assigned) + self.peak_real_pressure: int = 0 # max simultaneously live intervals including self-spilled + self._scratch_cache: dict[str, str] = {} # vreg -> last scratch reg for reload memory + + # ------------------------------------------------------------------ + # Live interval computation + # ------------------------------------------------------------------ + + def compute_live_intervals( + self, block: list[LsInstruction], + ) -> list[LiveInterval]: + """Compute live intervals for all virtual registers in a basic block. + + Parameters + ---------- + block: + List of LsInstruction objects in instruction order. + + Returns + ------- + List of LiveInterval objects sorted by start position. + """ + # Collect all virtual register names + vregs: set[str] = set() + for inst in block: + vregs |= inst.defines + vregs |= inst.uses + + intervals: list[LiveInterval] = [] + + for vreg in vregs: + start = -1 + end = -1 + uses = set() + + for inst in block: + if vreg in inst.defines: + if start == -1: + start = inst.id + if vreg in inst.uses: + uses.add(inst.id) + end = max(end, inst.id + 1) + if vreg in inst.defines and vreg in inst.uses: + # define and use in same instruction + uses.add(inst.id) + if start == -1: + start = inst.id + end = max(end, inst.id + 1) + + if start == -1: + start = 0 # live-in parameter + + if end == -1: + end = start + 1 + + intervals.append(LiveInterval( + vreg=vreg, start=start, end=end, uses=uses, + )) + + return sorted(intervals, key=lambda iv: iv.start) + + # ------------------------------------------------------------------ + # Linear scan allocation + # ------------------------------------------------------------------ + + def allocate(self, intervals: list[LiveInterval]) -> dict[str, str]: + """Perform linear scan register allocation. + + Parameters + ---------- + intervals: + Sorted list of live intervals (by start position). + + Returns + ------- + Mapping from virtual register name to physical register name. + """ + self.alloc_map.clear() + self.spill_code.clear() + self._spill_slots.clear() + self._reloads.clear() + self._spilled.clear() + self._evictions.clear() + self._intervals = intervals + self._vreg_interval = {iv.vreg: iv for iv in intervals} + self.peak_active = 0 + self.peak_real_pressure = 0 + + # Active list: (interval, phys_reg) sorted by increasing end + active: list[tuple[LiveInterval, str]] = [] + free_regs: list[str] = list(self.phys_regs) + + for interval in intervals: + # Expire old intervals + self._expire_old_intervals(active, interval.start, free_regs) + + if free_regs: + # Assign a free register + reg = free_regs.pop(0) + self.alloc_map[interval.vreg] = reg + active.append((interval, reg)) + else: + # Need to spill + spill = self.spill(interval, active, free_regs) + if spill is not None: + # Spill freed a register + reg = ( + free_regs.pop(0) if free_regs + else self.phys_regs[0] + ) + self.alloc_map[interval.vreg] = reg + active.append((interval, reg)) + + # Track peak pressure + current_active = len(active) + if current_active > self.peak_active: + self.peak_active = current_active + current_pressure = current_active + len(self._spilled) + if current_pressure > self.peak_real_pressure: + self.peak_real_pressure = current_pressure + + return dict(self.alloc_map) + + def _expire_old_intervals(self, active: list[tuple[LiveInterval, str]], + current_pos: int, + free_regs: list[str]) -> None: + """Remove intervals from active list that have ended.""" + i = 0 + while i < len(active): + interval, reg = active[i] + if interval.end <= current_pos: + free_regs.append(reg) + active.pop(i) + else: + i += 1 + + def spill(self, current: LiveInterval, + active: list[tuple[LiveInterval, str]], + free_regs: list[str]) -> Optional[str]: + """Select a register to spill and emit spill code. + + Chooses the active interval with the farthest end position to spill. + Records reload positions for the spilled interval so that + ``get_allocated_code`` can insert ``lw`` before each future use. + + Returns + ------- + The physical register freed by spilling, or None if current is spilled. + """ + if not active: + return None + + # Find the active interval with the farthest end + spill_idx = 0 + farthest_end = active[0][0].end + + for i, (interval, _) in enumerate(active): + if interval.end > farthest_end: + farthest_end = interval.end + spill_idx = i + + spill_interval, spill_reg = active[spill_idx] + + # Only spill if the current interval ends earlier + if current.end <= spill_interval.end: + # Spill the farthest active interval (victim) + slot = self._get_spill_slot(spill_interval.vreg) + active.pop(spill_idx) + self._evictions.setdefault(current.start, []).append( + f" sw {spill_reg}, {slot}(sp) # evict {spill_interval.vreg}" + ) + # Remove stale mapping so codegen won't use the freed register + self.alloc_map[spill_interval.vreg] = f"SPILL_{spill_interval.vreg}" + self._spilled.add(spill_interval.vreg) + # Record reload at every future use of the spilled vreg + for use_pos in spill_interval.uses: + if use_pos > current.start: + self._reloads.setdefault(use_pos, []).append( + (spill_interval.vreg, slot)) + free_regs.append(spill_reg) + return spill_reg + + # Otherwise, spill the current interval + slot = self._get_spill_slot(current.vreg) + # Assign a temporary register for the definition instruction + temp_reg = self.phys_regs[0] + self.alloc_map[current.vreg] = temp_reg + self._spilled.add(current.vreg) + self.spill_code.setdefault(current.start, []).append( + f" sw {temp_reg}, {slot}(sp) # spill {current.vreg}" + ) + for use_pos in current.uses: + if use_pos > current.start: + self._reloads.setdefault(use_pos, []).append( + (current.vreg, slot)) + return None + + def _get_spill_slot(self, vreg: str) -> int: + """Get or allocate a stack slot for a virtual register.""" + if vreg not in self._spill_slots: + self.stack_slot -= 4 + self._spill_slots[vreg] = self.stack_slot + return self._spill_slots[vreg] + + # ------------------------------------------------------------------ + # Code generation + # ------------------------------------------------------------------ + + def emit(self, block: list[LsInstruction]) -> str: + """Main entry point: allocate registers and emit assembly. + + Computes live intervals, runs linear-scan allocation, then + generates assembly with spill stores and reloads interleaved. + """ + intervals = self.compute_live_intervals(block) + self.allocate(intervals) + return self.get_allocated_code(block) + + def get_allocated_code(self, block: list[LsInstruction]) -> str: + """Generate allocated assembly with spill stores and reloads. + + Walks the instruction block in order. Before each instruction + that uses a spilled vreg, a reload ``lw`` is inserted. After + each instruction that defines a spilled vreg, a spill ``sw`` + is inserted. + """ + lines: list[str] = [] + rename: dict[str, str] = dict(self.alloc_map) + + for inst in block: + # Emit eviction spill stores before reloads at this position + if inst.id in self._evictions: + lines.extend(self._evictions[inst.id]) + + # Insert reloads before the instruction + if inst.id in self._reloads: + # Vregs used/defined by this instruction must not be evicted + # by _evict_for_reload, otherwise inst.to_asm() would get + # an unresolved vreg name. + protected: set[str] = inst.uses | inst.defines + for vreg, slot in self._reloads[inst.id]: + reload_reg = self._pick_reload_reg(rename, inst.id, protected) + lines.append( + f" lw {reload_reg}, {slot}(sp)" + f" # reload {vreg}" + ) + rename[vreg] = reload_reg + + # For spilled vregs defined here, pick a scratch register + for d in inst.defines: + if d in rename and rename[d].startswith("SPILL_"): + slot = self._spill_slots.get(d, 0) + scratch = self._pick_scratch(d) + rename[d] = scratch + + lines.append(inst.to_asm(rename)) + + # Insert spill stores after the instruction + if inst.id in self.spill_code: + lines.extend(self.spill_code[inst.id]) + + return "\n".join(lines) + + def _pick_reload_reg(self, rename: dict[str, str], current_pos: int, + protected_vregs: set[str] | None = None) -> str: + """Pick a free physical register for a reload ``lw``. + + Filters *rename* by actual liveness at *current_pos* so that + registers held by already-expired vregs are considered free. + If all registers are genuinely occupied, evicts the one whose + interval ends farthest away. + + *protected_vregs* are excluded from eviction — typically the + current instruction's own uses/defines — since evicting them + would leave the instruction with an unresolved vreg name. + """ + used: set[str] = set() + for vreg, preg in rename.items(): + interval = self._vreg_interval.get(vreg) + if interval is None or interval.contains(current_pos): + used.add(preg) + for reg in self.phys_regs: + if reg not in used: + return reg + return self._evict_for_reload(rename, used, current_pos, protected_vregs) + + def _evict_for_reload( + self, rename: dict[str, str], used: set[str], current_pos: int, + protected_vregs: set[str] | None = None, + ) -> str: + """Evict a live register to make room for a reload. + + Picks the vreg whose interval ends farthest away, generates a + spill store to its stack slot, and records future reloads for + its remaining uses. + + Vregs in *protected_vregs* are excluded from eviction — they are + needed by the instruction at *current_pos* and evicting them + would produce unresolved vreg names in the output. + """ + protect = protected_vregs or set() + farthest_vreg: str | None = None + farthest_end = -1 + for vreg, preg in rename.items(): + if preg not in used: + continue + if vreg in protect: + continue + interval = self._vreg_interval.get(vreg) + if interval is not None and interval.end > farthest_end: + farthest_end = interval.end + farthest_vreg = vreg + + if farthest_vreg is None: + return self.phys_regs[0] + + evicted_reg = rename[farthest_vreg] + slot = self._get_spill_slot(farthest_vreg) + self._spilled.add(farthest_vreg) + + # Emit spill store BEFORE the reload (evictions go before reloads) + self._evictions.setdefault(current_pos, []).append( + f" sw {evicted_reg}, {slot}(sp)" + f" # evict {farthest_vreg} for reload" + ) + + # Record future reloads for remaining uses of the evicted vreg + interval = self._vreg_interval.get(farthest_vreg) + if interval is not None: + for use_pos in interval.uses: + if use_pos > current_pos: + self._reloads.setdefault(use_pos, []).append( + (farthest_vreg, slot)) + + del rename[farthest_vreg] + return evicted_reg + + def _pick_scratch(self, vreg: str) -> str: + """Pick a scratch register for a spilled vreg definition. + + Uses a cache so the same vreg tends to get the same scratch reg, + reducing redundant loads in tight loops. + """ + if vreg in self._scratch_cache: + return self._scratch_cache[vreg] + busy: set[str] = set(self.alloc_map.values()) + for reg in self.phys_regs: + if reg not in busy: + self._scratch_cache[vreg] = reg + return reg + reg = self.phys_regs[0] + self._scratch_cache[vreg] = reg + return reg + + # ------------------------------------------------------------------ + # Report + # ------------------------------------------------------------------ + + def report(self) -> str: + """Return a string summary of the allocation result.""" + total = len(self.alloc_map) + spilled = len(self._spill_slots) + parts = [] + parts.append("Linear Scan Register Allocation Report") + parts.append(f" Virtual registers allocated: {total}") + parts.append(f" Stack spill slots used: {spilled}") + parts.append(f" Peak active (phys regs mapped): {self.peak_active}") + parts.append(f" Peak real pressure (incl. self-spilled): {self.peak_real_pressure}") + parts.append( + f" Physical registers available: {len(self.phys_regs)}" + ) + if self._spill_slots: + parts.append(" Spill details:") + for vreg, slot in self._spill_slots.items(): + parts.append(f" {vreg}: sp+{slot}") + return "\n".join(parts) + + +# --------------------------------------------------------------------------- +# Helper: convert MachineInstr list to LsInstruction list +# --------------------------------------------------------------------------- + +def block_from_machine_instrs( + instrs: list, # list of MachineInstr +) -> list[LsInstruction]: + """Convert MachineInstr list to LsInstruction list. + + Parameters + ---------- + instrs: + List of MachineInstr objects from register_alloc module. + + Returns + ------- + List of LsInstruction objects ready for linear scan allocator. + """ + result = [] + for i, mi in enumerate(instrs): + defines: set[str] = set() + uses: set[str] = set() + operands: list[str] = [] + + for op in (mi.dst, mi.src1, mi.src2): + if op is None: + continue + op_str = str(op).lstrip("%") + if op.kind == "vreg": + # For the destination operand position + if op is mi.dst: + defines.add(op_str) + operands.append(op_str) + else: + uses.add(op_str) + operands.append(op_str) + else: + operands.append(op_str) + + if mi.op.value == ".label": + result.append(LsInstruction( + id=i, opcode=".label", operands=[mi.comment], + comment=mi.comment, + )) + else: + result.append(LsInstruction( + id=i, + opcode=mi.op.value, + operands=operands, + defines=defines, + uses=uses, + comment=mi.comment, + )) + + return result + + +def machine_instrs_from_block( + block: list[LsInstruction], +) -> list: # list of MachineInstr + """Convert LsInstruction list back to MachineInstr list. + + This is the reverse of ``block_from_machine_instrs`` and enables the + linear-scan allocator's output to be consumed by ``AsmEmitter``. + + Parameters + ---------- + block: + List of LsInstruction objects (possibly after register renaming). + + Returns + ------- + List of MachineInstr objects. + """ + from scratchv.backend.machine_types import MachineInstr, MachineOp, MachineOperand + + result = [] + for inst in block: + if inst.opcode == ".label": + result.append(MachineInstr( + MachineOp.LABEL, comment=inst.comment, + )) + continue + + # Resolve opcode + try: + mop = MachineOp(inst.opcode) + except ValueError: + mop = MachineOp.MV # fallback + + # Build operands + def _to_mop(s: str) -> MachineOperand: + if s.startswith("x") or s.startswith("a") or s.startswith("t") or \ + s.startswith("s") or s.startswith("f") or s in ("zero", "ra", "sp", "gp", "tp", "fp"): + return MachineOperand.reg(s) + try: + return MachineOperand.immediate(int(s)) + except ValueError: + return MachineOperand.vreg(s) + + dst = None + src1 = None + src2 = None + ops = [_to_mop(o) for o in inst.operands] + if len(ops) >= 1: + dst = ops[0] + if len(ops) >= 2: + src1 = ops[1] + if len(ops) >= 3: + src2 = ops[2] + + result.append(MachineInstr(mop, dst, src1, src2, inst.comment)) + + return result \ No newline at end of file diff --git a/scratchv/backend/topic17_bottleneck_scenarios_v1.3.py b/scratchv/backend/topic17_bottleneck_scenarios_v1.3.py new file mode 100644 index 0000000..2882873 --- /dev/null +++ b/scratchv/backend/topic17_bottleneck_scenarios_v1.3.py @@ -0,0 +1,642 @@ +""" +课题17:寄存器分配瓶颈场景系统性测试 + +瓶颈分析维度(6大维度,每个维度2-4个具体场景): + 1. 活跃度维度 —— 活跃 vreg 数量 vs 物理寄存器数量 + 2. 生命期维度 —— 区间长度分布模式 + 3. 复用模式维度 —— 自溢 vreg 的访问频率和模式 + 4. eviction 模式维度 —— eviction 触发频率和 victim 选择 + 5. 栈压力维度 —— spill slot 复用效率 + 6. 代码膨胀维度 —— spill code 对最终代码大小的影响 + +每个场景输出: + - 场景含义(构造意图) + - 构建方式(具体代码) + - 详细测试数据 +""" +from scratchv.backend.regalloc_linear import LinearScanAllocator, LsInstruction +import random + + +PHYS_REGS = ['a0','a1','a2','a3','a4','a5','a6','a7', + 't0','t1','t2','t3','t4','t5','t6', + 's0','s1','s2','s3','s4','s5','s6','s7', + 's8','s9','s10','s11'] +POOL = len(PHYS_REGS) # 27 + + +def run_scenario(name, category, desc, block_fn, phys_regs=None): + """Run a single scenario and return all metrics.""" + regs = phys_regs or PHYS_REGS + alloc = LinearScanAllocator(phys_regs=regs) + try: + block = block_fn() + ivs = alloc.compute_live_intervals(block) + alloc.allocate(ivs) + code = alloc.get_allocated_code(block) + except Exception as e: + import traceback + return { + 'name': name, 'category': category, 'desc': desc, + 'pool': len(regs), 'error': str(e), + 'traceback': traceback.format_exc(), + } + + total_vregs = len(alloc.alloc_map) + spilled_self = sum(1 for v in alloc.alloc_map.values() + if isinstance(v, str) and v.startswith('SPILL_')) + # === Eviction vs self-spill counting === + # With Bug A fix (alloc_map update in spill()), both evicted and self-spilled + # vregs get "SPILL_" prefix in alloc_map. So we can't distinguish them by + # alloc_map alone. Instead, count evictions from spill_code entries: + # - eviction = spill_code entries with 'sw' (emitted by spill() for eviction) + # - self-spill = total spill_slots entries minus eviction sw count + evicted_sw_count = sum(1 for _pos, op, _operand in alloc.spill_code + if 'sw' in op) + self_spill = max(0, len(alloc._spill_slots) - evicted_sw_count) + evicted = evicted_sw_count + + # Code metrics + stores = code.count(' sw ') + loads = code.count(' lw ') + total_lines = len(code.split('\n')) + asm_size = len(code.encode('utf-8')) + + # Compute avg uses per vreg and other patterns + interval_map = {iv.vreg: iv for iv in ivs} + avg_uses = sum(len(iv.uses) for iv in ivs) / max(len(ivs), 1) + max_uses = max((len(iv.uses) for iv in ivs), default=0) + + # Count reloads to different scratch regs for same spilled vreg + # (spill_code efficiency: is a spilled vreg getting reloaded to + # different regs at different positions?) + spill_vreg_reloads = {} + for pos, op, operand in alloc.spill_code: + if 'lw' in op: + # operand format: "reg, offset(sp) # reload vXX" + parts = operand.split('#') + if len(parts) > 1: + vreg = parts[-1].strip().replace('reload ', '') + if vreg not in spill_vreg_reloads: + spill_vreg_reloads[vreg] = set() + reg = parts[0].strip().split(',')[0] + spill_vreg_reloads[vreg].add(reg) + + redundant_reloads = sum( + len(regs) - 1 for vreg, regs in spill_vreg_reloads.items() if len(regs) > 1 + ) + + # slot reuse efficiency + # Count how many times a slot at the same offset is used for different vregs + if alloc._spill_slots: + unique_slots = len(set(alloc._spill_slots.values())) + slot_reuse_ratio = 1.0 - (unique_slots / max(len(alloc._spill_slots), 1)) + else: + slot_reuse_ratio = 0.0 + + # alloc_map integrity check: any vreg leaking to asm? + vreg_leaks = [] + for line in code.split('\n'): + line = line.strip() + if not line or line.startswith('#'): + continue + for v in sorted(alloc.alloc_map.keys(), key=len, reverse=True): + if v in line and '# spill' not in line and '# reload' not in line: + vreg_leaks.append(f'{v} in "{line}"') + break + + # detect redundant sw (same slot, same value) + redundant_sw = 0 + sw_history = {} + for pos, op, operand in alloc.spill_code: + if 'sw' in op: + # operand: "reg, offset(sp) # spill vXX" + parts = operand.split(',') + if len(parts) >= 2: + slot = parts[1].strip().split('(')[0] + vpart = operand.split('#')[-1].strip().replace('spill ', '') + if slot in sw_history and sw_history[slot] == vpart: + redundant_sw += 1 + sw_history[slot] = vpart + + return { + 'name': name, + 'category': category, + 'desc': desc, + 'pool': len(regs), + 'total_vregs': total_vregs, + 'peak_active': alloc.peak_active, + 'self_spill': self_spill, + 'evicted': evicted, + 'spill_slots': len(alloc._spill_slots), + 'stores': stores, + 'loads': loads, + 'total_lines': total_lines, + 'asm_size_bytes': asm_size, + 'avg_uses': round(avg_uses, 2), + 'max_uses': max_uses, + 'redundant_reloads': redundant_reloads, + 'slot_reuse_ratio': round(slot_reuse_ratio, 3), + 'redundant_sw': redundant_sw, + 'vreg_leaks': vreg_leaks, + 'intervals': len(ivs), + 'block_len': len(block), + 'spill_code_entries': len(alloc.spill_code), + } + + +def print_result(r, verbose=False): + """Print formatted scenario result.""" + err = r.get('error') + if err: + print(f"\n{'='*72}") + print(f"[{r['name']}] {r['desc']}") + print(f" ERROR: {err}") + print(f" {r.get('traceback', '')}") + return + + flags = [] + if r['total_vregs'] > r['pool'] and r['peak_active'] <= r['pool']: + flags.append('PEAK_LOCKED') + if r['stores'] > 0 and r['loads'] == 0: + flags.append('STORE_ONLY') + if r['loads'] > r['stores']: + flags.append('LOADS>STORES') + if r['vreg_leaks']: + flags.append('VREG_LEAK') + if r['redundant_reloads'] > 0: + flags.append(f'REDUN_RELOAD({r["redundant_reloads"]})') + if r['redundant_sw'] > 0: + flags.append(f'REDUN_SW({r["redundant_sw"]})') + if r['slot_reuse_ratio'] < 0.01 and r['spill_slots'] > 1: + flags.append('SLOT_NO_REUSE') + + flag_str = f' !!! {", ".join(flags)}' if flags else '' + + print(f"\n{'='*72}") + print(f"[{r['name']}] {r['desc']} [{r['category']}]{flag_str}") + print(f"{'='*72}") + print(f" vregs={r['total_vregs']:>4d} pool={r['pool']:>2d} " + f"peak_active={r['peak_active']:>3d}/{r['pool']:>2d} " + f"block_len={r['block_len']:>4d}") + print(f" self_spill={r['self_spill']:>3d} evicted={r['evicted']:>3d} " + f"spill_slots={r['spill_slots']:>3d}") + print(f" stores(sw)={r['stores']:>4d} loads(lw)={r['loads']:>4d} " + f"spill_ops={r['spill_code_entries']:>4d}") + print(f" asm_lines={r['total_lines']:>4d} asm_bytes={r['asm_size_bytes']:>5d}") + print(f" avg_uses/vreg={r['avg_uses']:>5.2f} max_uses={r['max_uses']:>3d} " + f"intervals={r['intervals']:>4d}") + print(f" slot_reuse={r['slot_reuse_ratio']:.1%} redundant_sw={r['redundant_sw']:>3d} " + f"redundant_reloads={r['redundant_reloads']:>3d}") + if r['vreg_leaks']: + for leak in r['vreg_leaks'][:3]: + print(f" VREG LEAK: {leak}") + + +# ===================================================================== +# 瓶颈维度 1: 活跃度维度 +# 当活跃 vreg 数量超过物理寄存器时,分配器必须做出溢出决策。 +# 不同的溢出决策影响不同,测试各种 vreg/pool 比例下的瓶颈表现。 +# ===================================================================== + +def A01_slight_overlap(): + """轻度超压: pool+5 个 vreg 完全重叠,观察 entry-level 溢出行为""" + return ( + [LsInstruction(0, 'li', [f'v{i}', str(i)], defines={f'v{i}'}, uses=set()) + for i in range(POOL + 5)] + + [LsInstruction(1, 'add', ['v_out'] + [f'v{i}' for i in range(POOL + 5)], + defines={'v_out'}, uses={f'v{i}' for i in range(POOL + 5)})] + ) + +def A02_moderate_overlap(): + """中度超压: pool*2 vreg 完全重叠""" + return ( + [LsInstruction(0, 'li', [f'v{i}', str(i)], defines={f'v{i}'}, uses=set()) + for i in range(POOL * 2)] + + [LsInstruction(1, 'add', ['v_out'] + [f'v{i}' for i in range(POOL * 2)], + defines={'v_out'}, uses={f'v{i}' for i in range(POOL * 2)})] + ) + +def A03_severe_overlap(): + """重度超压: pool*7 vreg 完全重叠(200个)""" + return ( + [LsInstruction(0, 'li', [f'v{i}', str(i)], defines={f'v{i}'}, uses=set()) + for i in range(200)] + + [LsInstruction(1, 'add', ['v_out'] + [f'v{i}' for i in range(200)], + defines={'v_out'}, uses={f'v{i}' for i in range(200)})] + ) + +def A04_no_overlap(): + """零超压: 完全不重叠,验证无压力下的基线""" + return [ + inst + for i in range(POOL * 4) + for inst in [ + LsInstruction(i*2, 'li', [f'v{i}', str(i)], defines={f'v{i}'}, uses=set()), + LsInstruction(i*2+1, 'addi', [f'v{i}_2', f'v{i}', '1'], + defines={f'v{i}_2'}, uses={f'v{i}'}), + ] + ] + +# ===================================================================== +# 瓶颈维度 2: 生命期模式维度 +# 区间长度分布不同,影响 expire 和 eviction 的行为 +# ===================================================================== + +def B01_short_lived_dense(): + """200个密集短区间: 每个 vreg 只活一条指令,但同时在 pos 0 全部创建""" + return ( + [LsInstruction(0, 'li', [f'v{i}', str(i)], defines={f'v{i}'}, uses=set()) + for i in range(200)] + + [LsInstruction(1, 'addi', [f'v{i}_2', f'v{i}', '1'], + defines={f'v{i}_2'}, uses={f'v{i}'}) + for i in range(200)] + ) + +def B02_long_chain(): + """长依赖链: 200步链式依赖,vreg 长时间占用单个寄存器""" + return [ + LsInstruction(0, 'li', ['v0', '0'], defines={'v0'}, uses=set()) + ] + [ + LsInstruction(i, 'addi', ['v0', 'v0', '1'], defines={'v0'}, uses={'v0'}) + for i in range(1, 200) + ] + +def B03_mixed_lifetimes(): + """混合生命期: 3个长寿命 + 大量短寿命 vreg""" + def build(): + block = [LsInstruction(0, 'li', ['v_long1', '0'], defines={'v_long1'}, uses=set()), + LsInstruction(0, 'li', ['v_long2', '1'], defines={'v_long2'}, uses=set()), + LsInstruction(0, 'li', ['v_long3', '2'], defines={'v_long3'}, uses=set())] + # 中间插入 60 个短区间 + for i in range(60): + block.append(LsInstruction(1+i*2, 'li', [f'v{i}', str(i)], defines={f'v{i}'}, uses=set())) + block.append(LsInstruction(2+i*2, 'addi', [f'v{i}_2', f'v{i}', '1'], + defines={f'v{i}_2'}, uses={f'v{i}'})) + # 块尾使用长寿命 vreg + block.append(LsInstruction(200, 'add', ['v_out', 'v_long1', 'v_long2'], + defines={'v_out'}, uses={'v_long1', 'v_long2'})) + return block + return build() + +def B04_alternating_short(): + """交替短区间: 大量 vreg 交错定义和使用,制造频繁 expire""" + return [ + inst + for i in range(POOL * 6) + for inst in [ + LsInstruction(i, 'li', [f'v{i}', str(i)], defines={f'v{i}'}, uses=set()), + LsInstruction(i + POOL * 6, 'addi', [f'v{i}_2', f'v{i}', '1'], + defines={f'v{i}_2'}, uses={f'v{i}'}), + ] + ] + +# ===================================================================== +# 瓶颈维度 3: 复用模式维度 +# 自溢 vreg 被 reload 的次数和模式直接影响代码质量 +# ===================================================================== + +def C01_one_spill_many_uses(): + """单自溢vreg被频繁使用: 制造1个自溢vreg,然后在后续指令中重复使用它""" + def build(): + # 先用 POOL 个 vreg 占满寄存器 + block = [LsInstruction(0, 'li', [f'v{i}', str(i)], defines={f'v{i}'}, uses=set()) + for i in range(POOL)] + # 引入 POOL+1 个 vreg,确保 v{POOL} 自溢 + block += [LsInstruction(0, 'li', [f'v{POOL}', '99'], defines={f'v{POOL}'}, uses=set())] + block += [LsInstruction(1, 'add', ['v_sum'] + [f'v{i}' for i in range(POOL+1)], + defines={'v_sum'}, uses={f'v{i}' for i in range(POOL+1)})] + # 然后连续 20 次使用 v{POOL} + for i in range(20): + block.append(LsInstruction(2+i, 'addi', [f'v_out{i}', f'v{POOL}', str(i)], + defines={f'v_out{i}'}, uses={f'v{POOL}'})) + return block + return build() + +def C02_many_spills_one_use(): + """大量自溢vreg各用一次: 自溢 vreg 各自只被使用一次""" + def build(): + block = [LsInstruction(0, 'li', [f'v{i}', str(i)], defines={f'v{i}'}, uses=set()) + for i in range(POOL + 20)] + block += [LsInstruction(1, 'add', ['v_sum'] + [f'v{i}' for i in range(POOL + 20)], + defines={'v_sum'}, uses={f'v{i}' for i in range(POOL + 20)})] + # 每个自溢vreg只用一次 + for i in range(20): + block.append(LsInstruction(2+i, 'addi', [f'v_out{i}', f'v{POOL+i}', str(i)], + defines={f'v_out{i}'}, uses={f'v{POOL+i}'})) + return block + return build() + +def C03_spill_in_loop_body(): + """模拟循环体: 少量vreg + 重复定义使用模式""" + def build(): + block = [] + for iteration in range(30): + start = iteration * 5 + for j in range(10): + block.append(LsInstruction(start + j, 'li', [f'v{j}', str(j)], + defines={f'v{j}'}, uses=set())) + for j in range(8): + block.append(LsInstruction(start + 10 + j, 'add', [f'v_acc{j}', f'v{j}', f'v{j+1}'], + defines={f'v_acc{j}'}, uses={f'v{j}', f'v{j+1}'})) + return block + return build() + +def C04_spill_adjacent_uses(): + """同一自溢vreg在相邻位置被多次使用: 测试scratch寄存器复用""" + def build(): + block = [LsInstruction(0, 'li', [f'v{i}', str(i)], defines={f'v{i}'}, uses=set()) + for i in range(POOL + 3)] + block += [LsInstruction(1, 'add', ['v_sum'] + [f'v{i}' for i in range(POOL + 3)], + defines={'v_sum'}, uses={f'v{i}' for i in range(POOL + 3)})] + # v{POOL} 在同一指令中被用两次,然后连续被用 + block += [LsInstruction(2, 'add', ['v_out', f'v{POOL}', f'v{POOL}'], + defines={'v_out'}, uses={f'v{POOL}'})] + for i in range(10): + block.append(LsInstruction(3+i, 'add', [f'v_tmp{i}', f'v{POOL}', str(i)], + defines={f'v_tmp{i}'}, uses={f'v{POOL}'})) + return block + return build() + +# ===================================================================== +# 瓶颈维度 4: Eviction 模式维度 +# eviction 策略(farthest-end)在不同区间分布下的表现 +# ===================================================================== + +def D01_same_end_all(): + """全部同终点: 所有区间[0, end)相同,farthest-end 退化为随机选择""" + N = POOL + 20 + return ( + [LsInstruction(0, 'li', [f'v{i}', str(i)], defines={f'v{i}'}, uses=set()) + for i in range(N)] + + [LsInstruction(1, 'add', ['v_out'] + [f'v{i}' for i in range(N)], + defines={'v_out'}, uses={f'v{i}' for i in range(N)})] + ) + +def D02_skewed_ends(): + """偏斜终点: 前半区间短,后半区间长,观察 farthest-end 的偏差""" + N = POOL + 10 + def build(): + # 前半: 短区间 (用完后很快 expire) + block = [LsInstruction(i, 'li', [f'v{i}', str(i)], defines={f'v{i}'}, uses=set()) + for i in range(POOL // 2)] + block += [LsInstruction(POOL // 2, 'add', [f'v_sum_early'] + [f'v{i}' for i in range(POOL // 2)], + defines={'v_sum_early'}, uses={f'v{i}' for i in range(POOL // 2)})] + # 后半: 长区间 (活到块尾) + for i in range(POOL // 2, N): + block.append(LsInstruction(POOL // 2 + 1, 'li', [f'v{i}', str(i)], + defines={f'v{i}'}, uses=set())) + block.append(LsInstruction(POOL // 2 + 2, 'sub', ['v_out', f'v{POOL//2}', f'v{N-1}'], + defines={'v_out'}, uses={f'v{POOL//2}', f'v{N-1}'})) + return block + return build() + +def D03_eviction_chain(): + """eviction链式传递: 每引入一个新vreg就evict上一个,形成链""" + def build(): + block = [LsInstruction(i, 'li', [f'v{i}', str(i)], defines={f'v{i}'}, uses=set()) + for i in range(POOL)] + # 连续引入新vreg,每个立即使用前一个的值 + for w in range(30): + idx = POOL + w + block.append(LsInstruction(POOL + w*2, 'li', [f'v{idx}', str(w)], + defines={f'v{idx}'}, uses=set())) + block.append(LsInstruction(POOL + w*2 + 1, 'add', ['v_out', f'v{idx}', 'v0'], + defines={'v_out'}, uses={f'v{idx}', 'v0'})) + return block + return build() + +def D04_spiral_interleaving(): + """螺旋交织: 复杂的区间依赖关系,用模运算生成""" + rng = random.Random(42) + def build(): + block = [] + for i in range(100): + d = f'v{(i * 3) % 100}' + u1 = f'v{(i * 5) % 100}' + u2 = f'v{(i * 7) % 100}' + if d == u1 or d == u2: + d = f'v{i}' + u1 = f'v{(i + 1) % 100}' + u2 = f'v{(i + 2) % 100}' + block.append(LsInstruction(i, 'add', [d, u1, u2], defines={d}, uses={u1, u2})) + return block + return build() + +# ===================================================================== +# 瓶颈维度 5: 栈压力维度 +# spill slot 的分配和复用效率 +# ===================================================================== + +def E01_wave_spills(): + """10波交替溢出: 每波产生 POOL+3 个新vreg,然后全部释放""" + def build(): + block = [] + for wave in range(10): + base = wave * 30 + for i in range(POOL + 3): + block.append(LsInstruction(wave*2, 'li', [f'v{base+i}', str(i)], + defines={f'v{base+i}'}, uses=set())) + all_v = [f'v{base+i}' for i in range(POOL + 3)] + block.append(LsInstruction(wave*2+1, 'add', [f'v_sum{wave}'] + all_v, + defines={f'v_sum{wave}'}, uses=set(all_v))) + return block + return build() + +def E02_cascade_spills(): + """级联溢出: 每个新 vreg 依赖前一个的值,eviction 链不断延伸""" + def build(): + block = [LsInstruction(i, 'li', [f'v{i}', str(i)], defines={f'v{i}'}, uses=set()) + for i in range(POOL)] + for i in range(50): + idx = POOL + i + block.append(LsInstruction(POOL + i, 'mul', [f'v{idx}', f'v{idx-1}', f'v{idx-2}'], + defines={f'v{idx}'}, uses={f'v{idx-1}', f'v{idx-2}'})) + return block + return build() + +def E03_no_slot_reuse(): + """模拟栈槽完全不可复用: 所有区间几乎同时存活""" + return ( + [LsInstruction(0, 'li', [f'v{i}', str(i)], defines={f'v{i}'}, uses=set()) + for i in range(100)] + + [LsInstruction(1, 'add', ['v_out'] + [f'v{i}' for i in range(100)], + defines={'v_out'}, uses={f'v{i}' for i in range(100)})] + ) + +# ===================================================================== +# 瓶颈维度 6: 代码膨胀维度 +# spill code 对最终代码大小的影响 +# ===================================================================== + +def F01_max_spill_code(): + """最大溢出路径: 每个 vreg 都自溢+多次使用→大量sw/lw""" + def build(): + block = [LsInstruction(0, 'li', [f'v{i}', str(i)], defines={f'v{i}'}, uses=set()) + for i in range(100)] + for i in range(100): + for j in range(5): + block.append(LsInstruction(1 + i*5 + j, 'add', [f'v_tmp{i}_{j}', f'v{i}', str(j)], + defines={f'v_tmp{i}_{j}'}, uses={f'v{i}'})) + return block + return build() + +def F02_code_expansion_ratio(): + """代码膨胀比: 测量 asm 行数 / 原始 block 长度比""" + def build(): + ratio = 10 # 每个原始 vreg 对应 10 次使用 + block = [LsInstruction(0, 'li', [f'v{i}', str(i)], defines={f'v{i}'}, uses=set()) + for i in range(30)] + block += [LsInstruction(1, 'add', ['v_sum'] + [f'v{i}' for i in range(30)], + defines={'v_sum'}, uses={f'v{i}' for i in range(30)})] + for v in range(30): + for u in range(ratio): + block.append(LsInstruction(2 + v*ratio + u, 'addi', [f'v_out{v}_{u}', f'v{v}', str(u)], + defines={f'v_out{v}_{u}'}, uses={f'v{v}'})) + return block + return build() + +def F03_dense_chain(): + """链式密集全重叠 (dense_60 风格)""" + N = 60 + def build(): + block = [LsInstruction(0, 'li', [f'v{i}', str(i)], defines={f'v{i}'}, uses=set()) + for i in range(N)] + for i in range(N - 1): + block.append(LsInstruction(1 + i, 'add', [f'v{i}', f'v{i}', f'v{i+1}'], + defines={f'v{i}'}, uses={f'v{i}', f'v{i+1}'})) + return block + return build() + +def F04_large_random_block(): + """大规模随机块: 500条指令的完全随机模式""" + rng = random.Random(12345) + def build(): + block = [] + live_set = set() + active_vregs = set() + for i in range(500): + # 80% 概率使用已有 vreg,20% 概率创建新 vreg + if rng.random() < 0.2 or not active_vregs: + v = f'v{rng.randint(0, 299)}' + block.append(LsInstruction(i, 'li', [v, str(rng.randint(0, 99))], + defines={v}, uses=set())) + active_vregs.add(v) + else: + src1 = rng.choice(list(active_vregs)) + src2 = rng.choice(list(active_vregs)) + dst = f'v{rng.randint(0, 299)}' + block.append(LsInstruction(i, 'add', [dst, src1, src2], + defines={dst}, uses={src1, src2})) + active_vregs.add(dst) + # 随机 evict: 20% 概率释放一个 vreg + if active_vregs and rng.random() < 0.2: + victim = rng.choice(list(active_vregs)) + active_vregs.discard(victim) + return block + return build() + + +# ===================================================================== +# 运行所有场景 +# ===================================================================== + +if __name__ == '__main__': + all_scenarios = [ + # 维度 1: 活跃度 + ('A01', '活跃度', '轻度超压(pool+5,完全重叠)', A01_slight_overlap), + ('A02', '活跃度', '中度超压(pool*2,完全重叠)', A02_moderate_overlap), + ('A03', '活跃度', '重度超压(200vreg,完全重叠)', A03_severe_overlap), + ('A04', '活跃度', '零超压(pool*4,完全不重叠,基线)', A04_no_overlap), + + # 维度 2: 生命期模式 + ('B01', '生命期', '200密集短区间(同时创建)', B01_short_lived_dense), + ('B02', '生命期', '200步长依赖链(单vreg复用)', B02_long_chain), + ('B03', '生命期', '3长寿命+60短寿命混合', B03_mixed_lifetimes), + ('B04', '生命期', '交替短区间(pool*6个)', B04_alternating_short), + + # 维度 3: 复用模式 + ('C01', '复用模式', '单自溢vreg被20次连续使用', C01_one_spill_many_uses), + ('C02', '复用模式', '20个自溢vreg各用1次', C02_many_spills_one_use), + ('C03', '复用模式', '模拟循环体(30轮x10vreg)', C03_spill_in_loop_body), + ('C04', '复用模式', '自溢vreg相邻位置多次使用', C04_spill_adjacent_uses), + + # 维度 4: Eviction 模式 + ('D01', 'Eviction', '全部同终点(farthest-end退化为随机)', D01_same_end_all), + ('D02', 'Eviction', '偏斜终点(前短后长,farthest-end偏差)', D02_skewed_ends), + ('D03', 'Eviction', '链式eviction(每新vregevict上一个)', D03_eviction_chain), + ('D04', 'Eviction', '100步螺旋交织(复杂依赖)', D04_spiral_interleaving), + + # 维度 5: 栈压力 + ('E01', '栈压力', '10波交替溢出(波间可复用)', E01_wave_spills), + ('E02', '栈压力', '级联溢出(50步,eviction不断延伸)', E02_cascade_spills), + ('E03', '栈压力', '100vreg完全同时存活(零复用)', E03_no_slot_reuse), + + # 维度 6: 代码膨胀 + ('F01', '代码膨胀', '100vreg各用5次(最大溢出路径)', F01_max_spill_code), + ('F02', '代码膨胀', '30vreg各用10次(膨胀比测量)', F02_code_expansion_ratio), + ('F03', '代码膨胀', '60链式密集全重叠(dense_60)', F03_dense_chain), + ('F04', '代码膨胀', '500条指令完全随机块', F04_large_random_block), + ] + + print("=" * 72) + print("课题17: 寄存器分配瓶颈场景系统性测试") + print("=" * 72) + print(f"物理寄存器池: {POOL}个 ({', '.join(PHYS_REGS)})") + print(f"测试场景数: {len(all_scenarios)}") + print("=" * 72) + + results = [] + for sid, cat, desc, fn in all_scenarios: + name = f"{sid}_{fn.__name__}" + r = run_scenario(name, cat, desc, fn) + results.append(r) + print_result(r) + + # ================================================================ + # 汇总表 + # ================================================================ + print("\n\n") + print("=" * 120) + print("汇总表") + print("=" * 120) + header = f"{'场景':25s} {'类别':8s} {'vregs':>4s} {'pool':>3s} {'peak':>4s} {'self':>4s} {'evict':>4s} {'slots':>4s} {'sw':>4s} {'lw':>4s} {'lines':>5s} {'bytes':>6s} {'u/v':>5s} {'redund':>6s} {'s.reuse':>7s} {'flags':>20s}" + print(header) + print("-" * 120) + for r in results: + if r.get('error'): + print(f"{r['name']:25s} {'ERROR':>8s} {r.get('error', '')[:60]}") + continue + f = [] + if r['total_vregs'] > r['pool'] and r['peak_active'] <= r['pool']: + f.append('PKLOCK') + if r['vreg_leaks']: + f.append('VLEAK') + if r['redundant_sw'] > 0 and r['redundant_sw'] == r['spill_slots']: + f.append('ALL_REDUN_SW') + if r['redundant_reloads'] > 0: + f.append(f'RLOADx{r["redundant_reloads"]}') + if r['slot_reuse_ratio'] < 0.01 and r['spill_slots'] > 5: + f.append('NO_REUSE') + flags_s = ','.join(f[:3]) + print(f"{r['name']:25s} {r['category']:8s} {r['total_vregs']:>4d} {r['pool']:>3d} " + f"{r['peak_active']:>4d} {r['self_spill']:>4d} {r['evicted']:>4d} " + f"{r['spill_slots']:>4d} {r['stores']:>4d} {r['loads']:>4d} " + f"{r['total_lines']:>5d} {r['asm_size_bytes']:>6d} {r['avg_uses']:>5.2f} " + f"{r['redundant_reloads']:>6d} {r['slot_reuse_ratio']:>7.1%} {flags_s:>20s}") + + print("-" * 120) + + # ================================================================ + # 打印每个场景的详细构建代码 (供复现) + # ================================================================ + print("\n\n") + print("=" * 72) + print("附录: 每个场景的构建方式 (Python function)") + print("=" * 72) + for sid, cat, desc, fn in all_scenarios: + name = f"{sid}_{fn.__name__}" + import inspect + source = inspect.getsource(fn) + print(f"\n--- {name}: {desc} ---") + print(source) \ No newline at end of file