|
| 1 | +# 寄存器分配 Benchmark 设计文档 |
| 2 | + |
| 3 | +## 1. 概述 |
| 4 | + |
| 5 | +对 ScratchV 的**线性扫描寄存器分配器**(`LinearScanAllocator`)进行正确性与性能评估,并在 CNN 路径上与 LLVM 后端做指令数及寄存器溢出的交叉对比。 |
| 6 | + |
| 7 | +**设计目标**: |
| 8 | + |
| 9 | +- **正确性**:验证无溢出 / 有溢出两种场景的分配结果合法(无未解析 vreg、有效 opcode) |
| 10 | +- **性能**:测量分配耗时(均值 / 标准差)、活跃区间峰值压力 |
| 11 | +- **可对比**:每项输出统一的 `reg_spill_count` 指标,支持回归对比和筛选 |
| 12 | +- **跨后端对比**:同一 ONNX 模型经 ScratchV 和 LLVM 两条路径编译,对比静态指令数、opcode 类别分布、溢出/帧操作数量 |
| 13 | + |
| 14 | +入口:`benchmarks/test_regalloc/run_benchmark.py` |
| 15 | +单文件运行:直接执行 `bench_simple.py` / `bench_dense.py` / `bench_cnn.py` |
| 16 | + |
| 17 | +--- |
| 18 | + |
| 19 | +## 2. 架构 |
| 20 | + |
| 21 | +``` |
| 22 | +benchmarks/test_regalloc/ |
| 23 | +├── __init__.py |
| 24 | +├── bench_utils.py 共享常量(opcode 分类、callee-saved 集合)、llvmlite IR→RISC-V |
| 25 | +├── bench_simple.py Benchmark 1 — 无溢出正确性 |
| 26 | +├── bench_dense.py Benchmark 2 — 溢出正确性 |
| 27 | +├── bench_cnn.py Benchmark 3 — CNN 集成 + LLVM 对比 |
| 28 | +└── run_benchmark.py 运行器:汇总 3 路输出 → JSON / HTML / Markdown 报告 |
| 29 | +``` |
| 30 | + |
| 31 | +### 2.1 统一接口 |
| 32 | + |
| 33 | +每个 benchmark 文件导出: |
| 34 | + |
| 35 | +```python |
| 36 | +def run_bench(...) -> dict: |
| 37 | + """返回统一结构的统计 dict,必需键见 §4。""" |
| 38 | +``` |
| 39 | + |
| 40 | +运行器遍历三个 `run_bench()`,收集 dict 生成报告。 |
| 41 | + |
| 42 | +### 2.2 数据流 |
| 43 | + |
| 44 | +``` |
| 45 | + ┌──────────────────┐ |
| 46 | + │ run_benchmark.py │ |
| 47 | + └──────┬───────────┘ |
| 48 | + ┌─────────────┼─────────────┐ |
| 49 | + ▼ ▼ ▼ |
| 50 | + bench_simple bench_dense bench_cnn |
| 51 | + run_bench() run_bench() run_bench() |
| 52 | + │ │ │ |
| 53 | + ▼ ▼ ▼ |
| 54 | + LinearScanAllocator ... LinearScanAllocator |
| 55 | + .allocate() .allocate() |
| 56 | + .report() / to_dict() .report() / to_dict() |
| 57 | + │ │ │ │ |
| 58 | + ▼ ▼ ▼ ▼ |
| 59 | + {dict} {dict} {dict} {LLVM dict} |
| 60 | + │ │ │ │ |
| 61 | + └──────────────┴─────────┴─────────┘ |
| 62 | + │ |
| 63 | + ▼ |
| 64 | + JSON / HTML / MD 报告 |
| 65 | +``` |
| 66 | + |
| 67 | +--- |
| 68 | + |
| 69 | +## 3. 三项 Benchmark |
| 70 | + |
| 71 | +### 3.1 Benchmark 1 — 简单算术(无溢出) |
| 72 | + |
| 73 | +**文件**:`bench_simple.py` |
| 74 | +**目的**:验证物理寄存器充足时分配器产生零溢出。 |
| 75 | + |
| 76 | +| 参数 | 值 | |
| 77 | +|------|-----| |
| 78 | +| 虚拟寄存器 | 5 个(`v0`–`v4`) | |
| 79 | +| 物理寄存器池 | 8 个(`r0`–`r7`) | |
| 80 | +| 指令数 | 10 条(随机 add/sub/mul/and/or) | |
| 81 | +| 块生成 | `_gen_block(num_insts=10, num_vregs=5)` | |
| 82 | +| 断言 | `reg_spill_count == 0` → `valid = True` | |
| 83 | + |
| 84 | + |
| 85 | + |
| 86 | +### 3.2 Benchmark 2 — 密集计算(触发溢出) |
| 87 | + |
| 88 | +**文件**:`bench_dense.py` |
| 89 | +**目的**:人为制造高寄存器压力,迫使分配器溢出,验证溢出代码生成正确。 |
| 90 | + |
| 91 | +| 参数 | 值 | |
| 92 | +|------|-----| |
| 93 | +| 虚拟寄存器 | 30 个(`v0`–`v29`) | |
| 94 | +| 物理寄存器池 | 5 个(`r0`–`r4`) | |
| 95 | +| 指令数 | 80 条(30 条 define + 50 条交叉引用) | |
| 96 | +| 块生成 | 阶段 1: 逐个定义 vreg → 创建长 live range;阶段 2: 随机交叉引用保持活跃 | |
| 97 | +| 断言 | `reg_spill_count > 0` → `valid = True` | |
| 98 | + |
| 99 | + |
| 100 | + |
| 101 | +### 3.3 Benchmark 3 — CNN 集成 + LLVM 对比 |
| 102 | + |
| 103 | +**文件**:`bench_cnn.py` |
| 104 | +**目的**:通过真实 ONNX 模型编译流水线验证分配器,并与 LLVM 后端做指令数和溢出对比。 |
| 105 | + |
| 106 | +**ScratchV 编译流水线**: |
| 107 | + |
| 108 | +``` |
| 109 | +ONNXParser → IR Program (17 ops) |
| 110 | + → ConstantFolder → DeadCodeEliminator |
| 111 | + → InstructionSelector → MachineInstr (57, with vregs) |
| 112 | + → block_from_machine_instrs → [LsInstruction] |
| 113 | + → LinearScanAllocator.allocate() |
| 114 | + → get_allocated_code() → RISC-V 伪指令 |
| 115 | + → _validate_asm() # 检查无未解析 vreg、合法 opcode |
| 116 | +``` |
| 117 | + |
| 118 | +**LLVM 对比流水线**: |
| 119 | + |
| 120 | +``` |
| 121 | +convert_onnx_to_llvm(model) → LLVM IR (866K lines, 183MB) |
| 122 | + → llvmlite IR → RISC-V asm → 静态指令数 + opcode 分布 |
| 123 | + → 汇编启发式 → 溢出 slot / 帧操作 近似统计 |
| 124 | +``` |
| 125 | + |
| 126 | +| 参数 | 值 | |
| 127 | +|------|-----| |
| 128 | +| 模型 | `models/graph/cnn.onnx`(可 CLI 覆盖) | |
| 129 | +| IR 指令 | 17 条(3×conv + 3×relu + 3×maxpool + 2×gemm + sigmoid + 2×reshape) | |
| 130 | +| 物理寄存器 | `_INT_REGS`(28 个) | |
| 131 | +| ScratchV 输出 | ~57 条伪指令(mv/mul/add/slt/bnez…) | |
| 132 | +| LLVM 输出 | ~1099 条(RV64FD O2,真实循环展开) | |
| 133 | +| 断言 | `asm_valid == True` | |
| 134 | + |
| 135 | +> **注意**:ScratchV 侧输出 57 条**伪指令**(conv/maxpool 等语义级操作由仿真器实现),LLVM 侧输出 1099 条**自包含机器指令**(每个 conv 展开为 5 重嵌套循环的完整 RISC-V 指令序列)。`instr_ratio_fd ≈ 23.89x` 反映的是抽象层级差异而非优化能力差距,因此还提供 opcode **类别分布**作为跨层级可比指标。 |
| 136 | +
|
| 137 | +--- |
| 138 | + |
| 139 | +## 4. 指标规范 |
| 140 | + |
| 141 | +### 4.1 所有 benchmark 通用键 |
| 142 | + |
| 143 | +| 键 | 类型 | 来源 | 说明 | |
| 144 | +|----|------|------|------| |
| 145 | +| `mean_s` | `float` | `perf_counter` 均值 | 单次分配耗时(秒) | |
| 146 | +| `stdev_s` | `float` | `stdev` | 耗时标准差 | |
| 147 | +| `vreg_count` | `int` | `len(alloc.alloc_map)` | 已分配的虚拟寄存器数 | |
| 148 | +| `spills` | `int` | `len(alloc._spill_slots)` | 溢出 slot 数(别名) | |
| 149 | +| `reg_spill_count` | `int` | 同上 | **统一溢出指标键**(接口规范) | |
| 150 | +| `peak_active` | `int` | `alloc.peak_active` | 峰值同时活跃的物理寄存器数 | |
| 151 | +| `asm_lines` | `int` | `len(code.splitlines())` | 汇编输出行数 | |
| 152 | +| `valid` | `bool` | 由 `run_bench()` 设置 | 该项是否通过断言 | |
| 153 | + |
| 154 | +### 4.2 Benchmark 特有键 |
| 155 | + |
| 156 | +**bench_dense**: |
| 157 | + |
| 158 | +| 键 | 说明 | |
| 159 | +|----|------| |
| 160 | +| `reloads` | `lw ... # reload` 注释行数 | |
| 161 | + |
| 162 | +**bench_cnn (ScratchV 侧)**: |
| 163 | + |
| 164 | +| 键 | 来源 | 说明 | |
| 165 | +|----|------|------| |
| 166 | +| `vreg_total` | ONNXParser→isel | 编译流水线中出现的 vreg 总数 | |
| 167 | +| `ir_inst_count` | Program 指令计数 | IR 层操作数(=17) | |
| 168 | +| `machine_instrs` | `len(machine)` | MachineInstr 数量(=57) | |
| 169 | +| `sv_static_instrs` | `count_riscv_instrs()` | 汇编指令数(~46,不含标签/注释) | |
| 170 | +| `sv_cats` | `count_riscv_instrs()` | opcode 原始计数 | |
| 171 | +| `sv_cat_buckets` | `_op_categories()` | 6 类汇总(ALU/Load/Store/Branch/Mul/Other) | |
| 172 | +| `asm_errors` | `_validate_asm()` | 未解析 vreg / 未知 opcode 列表 | |
| 173 | +| `asm_valid` | `len(asm_errors) == 0` | 汇编合法性 | |
| 174 | +| `greedy_time_s` | Greedy allocator | Greedy分配器耗时(baseline) | |
| 175 | +| `greedy_out_instrs` | — | Greedy分配器输出指令数 | |
| 176 | + |
| 177 | +**bench_cnn (LLVM 对比侧)**: |
| 178 | + |
| 179 | +| 键 | 说明 | |
| 180 | +|----|------| |
| 181 | +| `llvm_im_instrs` | LLVM RV64IM 静态指令数 | |
| 182 | +| `llvm_fd_instrs` | LLVM RV64FD 静态指令数 | |
| 183 | +| `instr_ratio_fd` | `llvm_fd_instrs / max(sv_static_instrs, 1)` | |
| 184 | +| `llvm_fd_cats` | opcode 原始计数 | |
| 185 | +| `llvm_fd_cat_buckets` | 7 类汇总(+ Stack) | |
| 186 | +| `llvm_spill_slots` | 4 字节 sp 访问 ÷ 2(**近似**) | |
| 187 | +| `llvm_frame_save` | `sd` 到 callee-saved 的计数 | |
| 188 | +| `llvm_frame_restore` | `ld` 到 callee-saved 的计数 | |
| 189 | + |
| 190 | +### 4.3 `reg_spill_count` 规范 |
| 191 | + |
| 192 | +- **经过 regalloc 的路径**:直接取自 `alloc._spill_slots` 长度 → 精确值 |
| 193 | +- **不经过 regalloc 的路径**:LLVM 侧 `reg_spill_count` 是本路径的 ScratchV 精确值(0);LLVM 近似溢出独立为 `llvm_spill_slots`,不污染统一键 |
| 194 | +- **降级路径**:当 libLLVM 不可用时,`llvm_fd_instrs`/`llvm_spill_slots` 等键不存在于 dict 中,报告渲染 fallback 到 `"-"` |
| 195 | + |
| 196 | +--- |
| 197 | + |
| 198 | +## 5. LLVM 溢出统计 |
| 199 | + |
| 200 | +由于 libLLVM 缺少 regalloc pass 统计入口,LLVM 侧无法获取精确的寄存器溢出计数,通过汇编层面识别特定模式统计溢出: |
| 201 | + |
| 202 | +| 形态 | 正则 / 判定 | 含义 | |
| 203 | +|------|------------|------| |
| 204 | +| 帧保存 | `sd <callee-saved>, N(sp)` | prologue 保存 callee-saved 寄存器 | |
| 205 | +| 帧恢复 | `ld <callee-saved>, N(sp)` | epilogue 恢复 callee-saved 寄存器 | |
| 206 | +| 溢出 | `sw/lw/fsw/flw <reg>, N(sp)` | 寄存器值被 spill→reload(4 字节) | |
| 207 | + |
| 208 | +**callee-saved 集合**(RV64 ABI):`ra, fp, s0–s11, fs0–fs11` |
| 209 | + |
| 210 | +--- |
| 211 | + |
| 212 | +## 6. 输出格式 |
| 213 | + |
| 214 | +### 6.1 终端 |
| 215 | + |
| 216 | +``` |
| 217 | +============================================================ |
| 218 | + ScratchV — Register Allocation Benchmark Suite |
| 219 | +============================================================ |
| 220 | + 1. Simple: reg_spill_count=0, mean=0.019ms ✓ |
| 221 | + 2. Dense: reg_spill_count=15, mean=0.084ms ✓ |
| 222 | + 3. CNN: reg_spill_count=0, mean=0.052ms ✓ |
| 223 | + LLVM: RV64FD=1099 instrs (23.89x vs ScratchV 46) |
| 224 | +
|
| 225 | + Total: 25173.6ms PASS |
| 226 | +
|
| 227 | + JSON report: /tmp/rg_spill.json |
| 228 | + HTML report: /tmp/rg_spill.html |
| 229 | + Markdown: /tmp/rg_spill.md |
| 230 | +``` |
| 231 | + |
| 232 | +### 6.2 JSON |
| 233 | + |
| 234 | +标准化结构,`_` 前缀字段(如 asm 文本、allocator 实例)被排除: |
| 235 | + |
| 236 | +```json |
| 237 | +{ |
| 238 | + "timestamp": "2026-08-09T...", |
| 239 | + "total_time_s": 25.17, |
| 240 | + "repeats": 3, |
| 241 | + "results": { |
| 242 | + "1. Simple Arithmetic": { |
| 243 | + "mean_s": 1.9e-05, "reg_spill_count": 0, "peak_active": 5, |
| 244 | + "asm_lines": 10, "valid": true |
| 245 | + }, |
| 246 | + "2. Dense Computation": { |
| 247 | + "mean_s": 8.4e-05, "reg_spill_count": 15, "peak_active": 14, |
| 248 | + "asm_lines": 179, "reloads": 61, "valid": true |
| 249 | + }, |
| 250 | + "3. CNN Integration": { |
| 251 | + "mean_s": 5.2e-05, "reg_spill_count": 0, |
| 252 | + "sv_static_instrs": 46, "llvm_fd_instrs": 1099, |
| 253 | + "instr_ratio_fd": 23.89, "llvm_spill_slots": 87, |
| 254 | + "llvm_frame_save": 70, "llvm_frame_restore": 70, |
| 255 | + "llvm_fd_cat_buckets": {"ALU": 346, "Load": 98, ...}, |
| 256 | + "asm_valid": true, "valid": true |
| 257 | + } |
| 258 | + } |
| 259 | +} |
| 260 | +``` |
| 261 | + |
| 262 | +### 6.3 HTML / Markdown 汇总表 |
| 263 | + |
| 264 | +| Benchmark | Mean(ms) | Std(ms) | Vregs | Spills | Peak | Reloads | Asm | LLVM-FD | Ratio | LLVM-Spill | Valid | |
| 265 | +|-----------|----------|---------|-------|--------|------|---------|-----|---------|-------|------------|-------| |
| 266 | +| 1. Simple Arithmetic | 0.019 | 0.011 | 5 | 0 | 5 | - | 10 | - | - | - | ✓ | |
| 267 | +| 2. Dense Computation | 0.084 | 0.016 | 30 | 15 | 14 | 61 | 179 | - | - | - | ✓ | |
| 268 | +| 3. CNN Integration | 0.052 | 0.015 | 30 | 0 | 11 | - | 57 | 1099 | 23.89 | 87 | ✓ | |
| 269 | + |
| 270 | +--- |
| 271 | + |
| 272 | +## 7. 使用方式 |
| 273 | + |
| 274 | +```bash |
| 275 | +# 运行全部 3 项 benchmark,生成三格式报告 |
| 276 | +python benchmarks/test_regalloc/run_benchmark.py \ |
| 277 | + --repeats 30 \ |
| 278 | + --output-json report.json \ |
| 279 | + --output-html report.html \ |
| 280 | + --output-md report.md |
| 281 | + |
| 282 | +# 单独运行某项 |
| 283 | +python benchmarks/test_regalloc/bench_simple.py --repeats 100 |
| 284 | +python benchmarks/test_regalloc/bench_dense.py --repeats 50 |
| 285 | +python benchmarks/test_regalloc/bench_cnn.py \ |
| 286 | + --cnn-path models/graph/cnn.onnx --repeats 30 |
| 287 | +``` |
| 288 | + |
| 289 | +--- |
| 290 | + |
| 291 | +## 8. TODO |
| 292 | + |
| 293 | +1. **优化报告输出格式** |
| 294 | +2. **将通用的Benchmark组件进一步抽象到`bench_utils.py`当中** |
| 295 | +3. **目前ONNX模型的编译路径指令选择方面无法完全正确生成算子的汇编指令,与LLVM后端对比不公平** |
0 commit comments