From c45217885a1947bb486ba9e815bb01bc1daf5bca Mon Sep 17 00:00:00 2001 From: KangjieZhang Date: Mon, 3 Aug 2026 22:05:12 +0800 Subject: [PATCH 1/3] regalloc benchmark --- benchmarks/test_regalloc/bench_cnn.py | 325 ++++++++++++++++++ benchmarks/test_regalloc/bench_dense.py | 146 ++++++++ .../test_regalloc/bench_regalloc_linear.py | 183 ++++++++++ benchmarks/test_regalloc/bench_simple.py | 137 ++++++++ 4 files changed, 791 insertions(+) create mode 100644 benchmarks/test_regalloc/bench_cnn.py create mode 100644 benchmarks/test_regalloc/bench_dense.py create mode 100644 benchmarks/test_regalloc/bench_regalloc_linear.py create mode 100644 benchmarks/test_regalloc/bench_simple.py diff --git a/benchmarks/test_regalloc/bench_cnn.py b/benchmarks/test_regalloc/bench_cnn.py new file mode 100644 index 0000000..7a3b3cc --- /dev/null +++ b/benchmarks/test_regalloc/bench_cnn.py @@ -0,0 +1,325 @@ +# flake8: noqa +"""Benchmark 3 — CNN model integration with emulator verification. + +Compiles the CNN model through the full ScratchV pipeline, runs the +linear scan allocator, validates the output assembly, and optionally +verifies execution via the RV32 emulator. +""" + +import argparse +import os +import statistics +import sys +import time + +from scratchv.backend.regalloc_linear import ( + LinearScanAllocator, + block_from_machine_instrs, + _INT_REGS, +) +from scratchv.backend.register_alloc import RegisterAllocator + + +# --------------------------------------------------------------------------- +# Compilation helpers +# --------------------------------------------------------------------------- + + +def _compile_onnx(onnx_path: str) -> tuple: + """ONNX → IR → MachineInstr (with virtual registers). + + Returns ``(machine_instrs, ir_inst_count, vreg_count)``. + """ + from scratchv.frontend.onnx_parser import ONNXParser + from scratchv.backend.instruction_select import InstructionSelector + from scratchv.optimizer.constant_folding import ConstantFolder + from scratchv.optimizer.dead_code import DeadCodeEliminator + + program = ONNXParser().parse(onnx_path) + ir_count = sum( + 1 for f in program.functions for bb in f.blocks for _ in bb.instructions + ) + ConstantFolder(program).run() + DeadCodeEliminator(program).run() + machine = InstructionSelector(program).run() + + vregs: set[str] = set() + for mi in machine: + for op in (mi.dst, mi.src1, mi.src2): + if op and getattr(op, "kind", None) == "vreg": + vregs.add(str(op.value)) + return machine, ir_count, len(vregs) + + +# --------------------------------------------------------------------------- +# Assembly validation +# --------------------------------------------------------------------------- + +_KNOWN_OPS = { + "add", + "addi", + "sub", + "mul", + "div", + "rem", + "and", + "andi", + "or", + "ori", + "xor", + "xori", + "sll", + "slli", + "srl", + "srli", + "sra", + "srai", + "slt", + "slti", + "sltu", + "sltiu", + "lw", + "lh", + "lb", + "lbu", + "lhu", + "sw", + "sh", + "sb", + "beq", + "bne", + "blt", + "bge", + "bltu", + "bgeu", + "jal", + "jalr", + "auipc", + "lui", + "li", + "mv", + "nop", + "ret", + "bnez", + "j", + "max", + ".label", + ".text", + ".data", + ".global", + ".type", + "flw", + "fsw", + "fadd.s", + "fsub.s", + "fmul.s", + "fdiv.s", + "feq.s", + "flt.s", + "fle.s", + "fcvt.w.s", + "fcvt.s.w", +} + + +def _validate_asm(asm: str) -> list[str]: + """Check no unresolved vregs, valid opcodes.""" + errors: list[str] = [] + for lineno, line in enumerate(asm.splitlines(), start=1): + stripped = line.strip() + if not stripped or stripped.startswith("#"): + continue + content = stripped.lstrip() + if content.endswith(":") or not content: + continue + if "#" in content: + content = content[: content.index("#")].strip() + parts = content.split() + if not parts: + continue + if parts[0] not in _KNOWN_OPS: + errors.append(f"Line {lineno}: unknown opcode '{parts[0]}'") + for token in parts: + if token.startswith("v") and token[1:].isdigit(): + errors.append(f"Line {lineno}: unresolved vreg '{token}'") + return errors + + +# --------------------------------------------------------------------------- +# Emulator verification +# --------------------------------------------------------------------------- + + +def _run_emulator(cnn_path: str) -> dict: + """Compile *cnn_path* via standalone pipeline, run through RV32Emulator.""" + try: + from scratchv.standalone.onnx_to_riscv_standalone import ( + ONNXModel, + MemoryPlan, + CNNRISCVGenerator, + ) + from scratchv.simulator.rv32_emulator import RV32Emulator + except ImportError as e: + return {"passed": False, "error": f"import error: {e}"} + + try: + model = ONNXModel.from_file(cnn_path) + memory = MemoryPlan() + memory.layout_weights(model.initializers) + if model.inputs: + inp = model.inputs[0] + el = 1 + for d in model.get_shape(inp.name): + el *= d + memory.alloc_workspace(inp.name, el) + + generator = CNNRISCVGenerator(model, memory) + code_bytes = generator.generate() + + emu = RV32Emulator() + emu.load_code(code_bytes) + emu.run(max_instr=100_000) + return {"passed": True, "error": ""} + except Exception as exc: + return {"passed": False, "error": str(exc)[:120]} + + +# --------------------------------------------------------------------------- +# Benchmark +# --------------------------------------------------------------------------- + + +def bench_allocate(cnn_path: str, phys_regs: list[str], repeats: int = 30) -> dict: + """Full CNN compilation pipeline with linear scan allocator.""" + machine, ir_count, vreg_total = _compile_onnx(cnn_path) + block = block_from_machine_instrs(machine) + + times = [] + spill_counts = [] + + for _ in range(repeats): + alloc = LinearScanAllocator(phys_regs=phys_regs) + t0 = time.perf_counter() + alloc.allocate(alloc.compute_live_intervals(block)) + t1 = time.perf_counter() + times.append(t1 - t0) + spill_counts.append(len(alloc._spill_slots)) + + # Final run for stable stats + assembly validation + alloc = LinearScanAllocator(phys_regs=phys_regs) + alloc.allocate(alloc.compute_live_intervals(block)) + code = alloc.get_allocated_code(block) + asm_errors = _validate_asm(code) + + # Greedy allocator baseline + t0 = time.perf_counter() + greedy = RegisterAllocator(machine, mode="greedy") + greedy_out = greedy.run() + greedy_time = time.perf_counter() - t0 + + return { + "mean_s": statistics.mean(times), + "stdev_s": statistics.stdev(times) if len(times) > 1 else 0, + "vreg_total": vreg_total, + "ir_inst_count": ir_count, + "machine_instrs": len(machine), + "vreg_count": len(alloc.alloc_map), + "spills": spill_counts[-1], + "reg_spill_count": spill_counts[-1], + "peak_active": alloc.peak_active, + "asm_lines": len(code.splitlines()), + "asm_errors": asm_errors, + "asm_valid": len(asm_errors) == 0, + "greedy_time_s": greedy_time, + "greedy_out_instrs": len(greedy_out), + "_report": alloc.report(), + "_alloc": alloc, + } + + +def run_bench( + cnn_path: str, phys_regs: list[str] | None = None, repeats: int = 30 +) -> dict: + """Entry point for the test suite runner.""" + if phys_regs is None: + phys_regs = list(_INT_REGS) + stats = bench_allocate(cnn_path, phys_regs, repeats=repeats) + + # Emulator verification (non-fatal) + emu = _run_emulator(cnn_path) + stats["emu_passed"] = emu["passed"] + stats["emu_error"] = emu.get("error", "") + stats["valid"] = stats["asm_valid"] + return stats + + +# --------------------------------------------------------------------------- +# CLI +# --------------------------------------------------------------------------- + + +def main(): + parser = argparse.ArgumentParser(description="Benchmark 3 — CNN Model Integration") + parser.add_argument( + "--repeats", type=int, default=30, help="Number of repeat measurements" + ) + parser.add_argument("--cnn-path", default="", help="Path to ONNX model") + + args = parser.parse_args() + + if not args.cnn_path: + args.cnn_path = os.path.join( + os.path.dirname(__file__), + "..", + "..", + "models", + "graph", + "cnn.onnx", + ) + + phys_regs = list(_INT_REGS) + + print("=" * 60) + print("Benchmark 3 — CNN Model Integration") + print(f" Model: {os.path.basename(args.cnn_path)}") + print("=" * 60) + + stats = run_bench(args.cnn_path, phys_regs=phys_regs, repeats=args.repeats) + + print( + f"\n{'':>8} {'Mean(ms)':>10} {'Stdev(ms)':>10} " + f"{'Vregs':>6} {'Spills':>7} {'Peak':>6} {'Asm':>5}" + ) + print("-" * 55) + print( + f"{'cnn':>8} {stats['mean_s'] * 1000:>10.3f} " + f"{stats['stdev_s'] * 1000:>10.3f} " + f"{stats['vreg_count']:>6} {stats['spills']:>7} " + f"{stats['peak_active']:>6} {stats['asm_lines']:>5}" + ) + + print() + print(stats["_report"]) + print( + f" Greedy baseline: {stats['greedy_time_s'] * 1000:.3f}ms, " + f"{stats['greedy_out_instrs']} instrs" + ) + + if not stats["asm_valid"]: + for e in stats["asm_errors"][:3]: + print(f" ✗ {e}") + if not stats["emu_passed"]: + print(f" Emulator: ✗ {stats['emu_error']}") + else: + print(f" Emulator: ✓ passed") + + asm_ok = "PASS" if stats["asm_valid"] else "FAIL" + print( + f"\n asm_valid={stats['asm_valid']}, " + f"reg_spill_count={stats['spills']} [{asm_ok}]" + ) + return 0 if stats["asm_valid"] else 1 + + +if __name__ == "__main__": + sys.exit(main() or 0) diff --git a/benchmarks/test_regalloc/bench_dense.py b/benchmarks/test_regalloc/bench_dense.py new file mode 100644 index 0000000..87a1812 --- /dev/null +++ b/benchmarks/test_regalloc/bench_dense.py @@ -0,0 +1,146 @@ +# flake8: noqa +"""Benchmark 2 — Dense computation (30 vregs, triggers spilling). + +Forces the linear scan allocator to spill by providing more virtual +registers than physical registers. +""" + +import argparse +import os +import random +import statistics +import sys +import time + +from scratchv.backend.regalloc_linear import LinearScanAllocator, LsInstruction + + +def _gen_block( + num_insts: int = 80, num_vregs: int = 30, seed: int = 42 +) -> list[LsInstruction]: + """Generate a high-register-pressure block.""" + random.seed(seed) + ops = ["add", "sub", "mul", "and", "or", "xor", "sll", "srl"] + vreg_names = [f"v{i}" for i in range(num_vregs)] + insts = [] + + # Phase 1: define each vreg — creates long live ranges + for i in range(num_vregs): + insts.append( + LsInstruction( + id=i, + opcode="addi", + operands=[vreg_names[i], "zero", str(random.randint(1, 100))], + defines={vreg_names[i]}, + uses=set(), + comment=f"def {vreg_names[i]}", + ) + ) + + # Phase 2: cross-reference dense ops — keeps many vregs live + for i in range(num_vregs, num_insts): + src1 = random.choice(vreg_names) + src2 = random.choice(vreg_names) + dst = random.choice(vreg_names) + insts.append( + LsInstruction( + id=i, + opcode=random.choice(ops), + operands=[dst, src1, src2], + defines={dst}, + uses={src1, src2}, + comment=f"dense op {i}", + ) + ) + return insts + + +def bench_allocate( + block: list[LsInstruction], phys_regs: list[str], repeats: int = 30 +) -> dict: + """Benchmark the full allocation pipeline under register pressure.""" + times = [] + spill_counts = [] + + for _ in range(repeats): + alloc = LinearScanAllocator(phys_regs=phys_regs) + t0 = time.perf_counter() + alloc.allocate(alloc.compute_live_intervals(block)) + t1 = time.perf_counter() + times.append(t1 - t0) + spill_counts.append(len(alloc._spill_slots)) + + # Final run for stable stats + alloc = LinearScanAllocator(phys_regs=phys_regs) + alloc.allocate(alloc.compute_live_intervals(block)) + code = alloc.get_allocated_code(block) + reloads = sum( + 1 for ln in code.splitlines() if ln.strip().startswith("lw ") and "reload" in ln + ) + + return { + "mean_s": statistics.mean(times), + "stdev_s": statistics.stdev(times) if len(times) > 1 else 0, + "vreg_count": len(alloc.alloc_map), + "spills": spill_counts[-1], + "reg_spill_count": spill_counts[-1], + "peak_active": alloc.peak_active, + "asm_lines": len(code.splitlines()), + "reloads": reloads, + "_report": alloc.report(), + "_alloc": alloc, + } + + +def run_bench(phys_regs: list[str] | None = None, repeats: int = 30) -> dict: + """Entry point for the test suite runner.""" + if phys_regs is None: + phys_regs = [f"r{i}" for i in range(5)] + block = _gen_block(num_insts=80, num_vregs=30) + stats = bench_allocate(block, phys_regs, repeats=repeats) + stats["valid"] = stats["spills"] > 0 + return stats + + +def main(): + parser = argparse.ArgumentParser( + description="Benchmark 2 — Dense Computation (spills)" + ) + parser.add_argument( + "--repeats", type=int, default=30, help="Number of repeat measurements" + ) + args = parser.parse_args() + # limit to 5 phys_regs + phys_regs = [f"r{i}" for i in range(5)] + + print("=" * 60) + print("Benchmark 2 — Dense Computation (30 vregs / 5 phys regs)") + print("=" * 60) + + stats = run_bench(phys_regs=phys_regs, repeats=args.repeats) + + print( + f"\n{'':>8} {'Mean(ms)':>10} {'Stdev(ms)':>10} " + f"{'Vregs':>6} {'Spills':>7} {'Peak':>6} {'Reloads':>8} " + f"{'Asm':>5}" + ) + print("-" * 65) + print( + f"{'dense':>8} {stats['mean_s'] * 1000:>10.3f} " + f"{stats['stdev_s'] * 1000:>10.3f} " + f"{stats['vreg_count']:>6} {stats['spills']:>7} " + f"{stats['peak_active']:>6} {stats['reloads']:>8} " + f"{stats['asm_lines']:>5}" + ) + + print() + print(stats["_report"]) + + spills = stats["spills"] + ok = "PASS" if spills > 0 else "FAIL (expected spills)" + print(f"\n reg_spill_count={spills} [{ok}]") + return 0 if spills > 0 else 1 + + +if __name__ == "__main__": + sys.exit(main() or 0) diff --git a/benchmarks/test_regalloc/bench_regalloc_linear.py b/benchmarks/test_regalloc/bench_regalloc_linear.py new file mode 100644 index 0000000..ffd4b36 --- /dev/null +++ b/benchmarks/test_regalloc/bench_regalloc_linear.py @@ -0,0 +1,183 @@ +#!/usr/bin/env python3 +"""Run all 3 register allocation benchmarks and produce a report.""" + +import argparse +import datetime +import json +import os +import sys +import time + + +from benchmarks.test_regalloc import bench_simple, bench_dense, bench_cnn + + +# --------------------------------------------------------------------------- +# Report helpers +# --------------------------------------------------------------------------- + + +def _make_html(results: dict, total_time: float) -> str: + """Generate an HTML report.""" + rows = "" + for name, r in results.items(): + if not isinstance(r, dict): + continue + v = "✓" if r.get("valid", True) else "✗" + c = "#22863a" if r.get("valid", True) else "#cb2431" + ms = f"{r.get('mean_s', 0) * 1000:.3f}" + sd = f"{r.get('stdev_s', 0) * 1000:.3f}" + rows += ( + f"{name}{ms}{sd}" + f"{r.get('vreg_count', '-')}" + f"{r.get('reg_spill_count', r.get('spills', '-'))}" + f"{r.get('peak_active', '-')}" + f"{r.get('reloads', '-')}" + f"{r.get('asm_lines', '-')}" + f"{v}\n" + ) + + return f""" + + + +Register Allocation Benchmark Report + + + +

Register Allocation Benchmark Report

+

Generated: {datetime.datetime.now().isoformat()} | Total: {total_time * 1000:.1f}ms

+ + + +{rows} +
BenchmarkMean(ms)Std(ms)VregsSpillsPeakReloadsAsmValid
+ +""" + + +def _make_markdown(results: dict) -> str: + """Generate a Markdown report.""" + lines = [ + "# Register Allocation Benchmark Report", + "", + f"**Generated**: {datetime.datetime.now().isoformat()}", + "", + "| Benchmark | Mean(ms) | Std(ms) | Vregs | Spills | Peak | " + "Reloads | Asm | Valid |", + "|-----------|----------|---------|-------|--------|------|" + "---------|-----|-------|", + ] + for name, r in results.items(): + if not isinstance(r, dict): + continue + ms = f"{r.get('mean_s', 0) * 1000:.3f}" + sd = f"{r.get('stdev_s', 0) * 1000:.3f}" + v = "✓" if r.get("valid", True) else "✗" + lines.append( + f"| {name} | {ms} | {sd} | {r.get('vreg_count', '-')} | " + f"{r.get('reg_spill_count', r.get('spills', '-'))} | " + f"{r.get('peak_active', '-')} | " + f"{r.get('reloads', '-')} | {r.get('asm_lines', '-')} | " + f"{v} |" + ) + return "\n".join(lines) + + +# --------------------------------------------------------------------------- +# Runner +# --------------------------------------------------------------------------- + + +def main(): + parser = argparse.ArgumentParser(description="Register Allocation Benchmark Suite") + parser.add_argument("--repeats", type=int, default=30) + parser.add_argument("--output-json", default="") + parser.add_argument("--output-html", default="") + parser.add_argument("--output-md", default="") + args = parser.parse_args() + + print("=" * 60) + print(" ScratchV — Register Allocation Benchmark Suite") + print("=" * 60) + + t0 = time.perf_counter() + results: dict = {} + + # Benchmark 1 — Simple (no-spill) + r1 = bench_simple.run_bench(repeats=args.repeats) + results["1. Simple Arithmetic"] = r1 + print( + f" 1. Simple: reg_spill_count={r1['reg_spill_count']}, " + f"mean={r1['mean_s'] * 1000:.3f}ms " + f"{'✓' if r1.get('valid') else '✗'}" + ) + + # Benchmark 2 — Dense (spill) + r2 = bench_dense.run_bench(repeats=args.repeats) + results["2. Dense Computation"] = r2 + print( + f" 2. Dense: reg_spill_count={r2['reg_spill_count']}, " + f"mean={r2['mean_s'] * 1000:.3f}ms " + f"{'✓' if r2.get('valid') else '✗'}" + ) + + # Benchmark 3 — CNN Integration + cnn_default = "models/graph/cnn.onnx" + r3 = bench_cnn.run_bench(cnn_path=cnn_default, repeats=args.repeats) + results["3. CNN Integration"] = r3 + print( + f" 3. CNN: reg_spill_count={r3['reg_spill_count']}, " + f"mean={r3['mean_s'] * 1000:.3f}ms " + f"{'✓' if r3.get('valid') else '✗'}" + ) + + total_time = time.perf_counter() - t0 + + # Summary + all_ok = all(r.get("valid", True) for r in results.values() if isinstance(r, dict)) + print(f"\n Total: {total_time * 1000:.1f}ms {'PASS' if all_ok else 'HAD ERRORS'}") + + # Reports + if args.output_json: + report = { + "timestamp": datetime.datetime.now().isoformat(), + "total_time_s": total_time, + "repeats": args.repeats, + "results": { + name: { + k: v + for k, v in r.items() + if not k.startswith("_") and k != "asm_errors" + } + for name, r in results.items() + if isinstance(r, dict) + }, + } + with open(args.output_json, "w") as f: + json.dump(report, f, indent=2) + print(f"\n JSON report: {args.output_json}") + + if args.output_html: + with open(args.output_html, "w") as f: + f.write(_make_html(results, total_time)) + print(f" HTML report: {args.output_html}") + + if args.output_md: + with open(args.output_md, "w") as f: + f.write(_make_markdown(results)) + print(f" Markdown: {args.output_md}") + + return 0 if all_ok else 1 + + +if __name__ == "__main__": + sys.exit(main() or 0) + diff --git a/benchmarks/test_regalloc/bench_simple.py b/benchmarks/test_regalloc/bench_simple.py new file mode 100644 index 0000000..abfadff --- /dev/null +++ b/benchmarks/test_regalloc/bench_simple.py @@ -0,0 +1,137 @@ +# flake8: noqa +"""Benchmark 1 — Simple arithmetic (3-5 vregs, no spilling). + +Verifies that the linear scan allocator produces zero spills when +physical registers are plentiful. +""" + +import argparse +import os +import random +import statistics +import sys +import time + +from scratchv.backend.regalloc_linear import LinearScanAllocator, LsInstruction + + +def _gen_block( + num_insts: int = 10, num_vregs: int = 5, seed: int = 42 +) -> list[LsInstruction]: + """Generate a basic block with simple arithmetic using few vregs.""" + random.seed(seed) + ops = ["add", "sub", "mul", "and", "or"] + vreg_names = [f"v{i}" for i in range(num_vregs)] + insts = [] + + for i in range(num_insts): + if i < num_vregs: + dst = vreg_names[i] + pool = vreg_names[: max(i, 1)] + src1 = random.choice(pool) + src2 = random.choice(pool) + insts.append( + LsInstruction( + id=i, + opcode=random.choice(ops), + operands=[dst, src1, src2], + defines={dst}, + uses={src1, src2}, + ) + ) + else: + dst = random.choice(vreg_names) + src1 = random.choice(vreg_names) + src2 = random.choice(vreg_names) + insts.append( + LsInstruction( + id=i, + opcode=random.choice(ops), + operands=[dst, src1, src2], + defines={dst}, + uses={src1, src2} - {dst}, + ) + ) + return insts + + +def bench_allocate( + block: list[LsInstruction], phys_regs: list[str], repeats: int = 50 +) -> dict: + """Benchmark the full allocation pipeline.""" + times = [] + spill_counts = [] + + for _ in range(repeats): + alloc = LinearScanAllocator(phys_regs=phys_regs) + t0 = time.perf_counter() + alloc.allocate(alloc.compute_live_intervals(block)) + t1 = time.perf_counter() + times.append(t1 - t0) + spill_counts.append(len(alloc._spill_slots)) + + # One final run for stable stats + alloc = LinearScanAllocator(phys_regs=phys_regs) + alloc.allocate(alloc.compute_live_intervals(block)) + code = alloc.get_allocated_code(block) + + return { + "mean_s": statistics.mean(times), + "stdev_s": statistics.stdev(times) if len(times) > 1 else 0, + "vreg_count": len(alloc.alloc_map), + "spills": spill_counts[-1], + "reg_spill_count": spill_counts[-1], + "peak_active": alloc.peak_active, + "asm_lines": len(code.splitlines()), + "_report": alloc.report(), + "_alloc": alloc, + } + + +def run_bench(phys_regs: list[str] | None = None, repeats: int = 50) -> dict: + """Entry point for the test suite runner.""" + if phys_regs is None: + phys_regs = [f"r{i}" for i in range(8)] + block = _gen_block(num_insts=10, num_vregs=5) + stats = bench_allocate(block, phys_regs, repeats=repeats) + stats["valid"] = stats["spills"] == 0 + return stats + + +def main(): + parser = argparse.ArgumentParser( + description="Benchmark 1 — Simple Arithmetic (no-spill)" + ) + parser.add_argument( + "--repeats", type=int, default=50, help="Number of repeat measurements" + ) + args = parser.parse_args() + + phys_regs = [f"r{i}" for i in range(8)] + + print("=" * 60) + print("Benchmark 1 — Simple Arithmetic (5 vregs / 8 phys regs)") + print("=" * 60) + + stats = run_bench(phys_regs=phys_regs, repeats=args.repeats) + + print(f"\n{'':>8} {'Mean(ms)':>10} {'Stdev(ms)':>10} {'Vregs':>6} {'Spills':>7}") + print("-" * 50) + print( + f"{'simple':>8} {stats['mean_s'] * 1000:>10.3f} " + f"{stats['stdev_s'] * 1000:>10.3f} " + f"{stats['vreg_count']:>6} {stats['spills']:>7}" + ) + + print() + print(stats["_report"]) + + spills = stats["spills"] + ok = "PASS" if spills == 0 else "FAIL" + print(f"\n reg_spill_count={spills} [{ok}]") + return 0 if spills == 0 else 1 + + +if __name__ == "__main__": + sys.exit(main() or 0) + From 6ef7dde80c730587278df9e0968aab01780e29f3 Mon Sep 17 00:00:00 2001 From: KangjieZhang Date: Sun, 9 Aug 2026 16:57:25 +0800 Subject: [PATCH 2/3] Add LLVM Comparison --- benchmarks/test_regalloc/__init__.py | 20 ++ benchmarks/test_regalloc/bench_cnn.py | 199 ++++++++----- benchmarks/test_regalloc/bench_dense.py | 2 +- .../test_regalloc/bench_regalloc_linear.py | 3 +- benchmarks/test_regalloc/bench_simple.py | 1 - benchmarks/test_regalloc/bench_utils.py | 263 ++++++++++++++++++ 6 files changed, 414 insertions(+), 74 deletions(-) create mode 100644 benchmarks/test_regalloc/__init__.py create mode 100644 benchmarks/test_regalloc/bench_utils.py diff --git a/benchmarks/test_regalloc/__init__.py b/benchmarks/test_regalloc/__init__.py new file mode 100644 index 0000000..287906d --- /dev/null +++ b/benchmarks/test_regalloc/__init__.py @@ -0,0 +1,20 @@ +# flake8: noqa +""" +Register Allocation Benchmarks for ScratchV. + +This package contains three benchmark suites for the linear scan +register allocator (``LinearScanAllocator``): + +1. **bench1_simple** — Simple arithmetic (3-5 vregs, no spills) +2. **bench2_dense** — Dense computation (20+ vregs, triggers spilling) +3. **bench3_cnn** — CNN model integration (operations from ``models/graph/cnn.onnx``) + +Each suite measures allocation time, spill count, peak register pressure, +and validates output correctness. + +Usage:: + + python -m benchmarks.test_regalloc.run_all +""" + +from __future__ import annotations diff --git a/benchmarks/test_regalloc/bench_cnn.py b/benchmarks/test_regalloc/bench_cnn.py index 7a3b3cc..0f34eb4 100644 --- a/benchmarks/test_regalloc/bench_cnn.py +++ b/benchmarks/test_regalloc/bench_cnn.py @@ -8,6 +8,7 @@ import argparse import os +import re import statistics import sys import time @@ -18,6 +19,7 @@ _INT_REGS, ) from scratchv.backend.register_alloc import RegisterAllocator +from scratchv.standalone.compare_codegen import count_riscv_instrs # --------------------------------------------------------------------------- @@ -54,72 +56,7 @@ def _compile_onnx(onnx_path: str) -> tuple: # --------------------------------------------------------------------------- # Assembly validation # --------------------------------------------------------------------------- - -_KNOWN_OPS = { - "add", - "addi", - "sub", - "mul", - "div", - "rem", - "and", - "andi", - "or", - "ori", - "xor", - "xori", - "sll", - "slli", - "srl", - "srli", - "sra", - "srai", - "slt", - "slti", - "sltu", - "sltiu", - "lw", - "lh", - "lb", - "lbu", - "lhu", - "sw", - "sh", - "sb", - "beq", - "bne", - "blt", - "bge", - "bltu", - "bgeu", - "jal", - "jalr", - "auipc", - "lui", - "li", - "mv", - "nop", - "ret", - "bnez", - "j", - "max", - ".label", - ".text", - ".data", - ".global", - ".type", - "flw", - "fsw", - "fadd.s", - "fsub.s", - "fmul.s", - "fdiv.s", - "feq.s", - "flt.s", - "fle.s", - "fcvt.w.s", - "fcvt.s.w", -} +from .bench_utils import _KNOWN_OPS def _validate_asm(asm: str) -> list[str]: @@ -184,6 +121,89 @@ def _run_emulator(cnn_path: str) -> dict: return {"passed": False, "error": str(exc)[:120]} +# --------------------------------------------------------------------------- +# LLVM comparison +# --------------------------------------------------------------------------- + +# Instruction category buckets used to compare opcode mixes between the +# ScratchV backend and the LLVM backend. `sd`/`ld` are ABI stack +# save/restore pairs — the classic LLVM frame-management cost. +from .bench_utils import ( + _CAT_ALU, + _CAT_LOAD, + _CAT_STORE, + _CAT_BRANCH, + _CAT_MUL, + _CAT_STACK, +) +from .bench_utils import _op_categories + +# RV64 ABI callee-saved registers — `sd`/`ld` to these at sp offsets are +# prologue/epilogue frame save/restore, not spills. +from .bench_utils import _CALLEE_SAVED + + +def _llvm_spill_stats(asm: str) -> dict: + """Approximate LLVM spill/frame stats from RISC-V assembly. + + libLLVM codegen does not expose regalloc pass statistics, so spill + counts are inferred from sp-based memory accesses: + - ``sd``/``ld`` to callee-saved regs → ABI frame save/restore + - 4-byte ``sw``/``lw``/``fsw``/``flw`` → spilled values + (each spill site emits a store + reload pair) + + Returns ``{llvm_spill_slots, llvm_frame_save, llvm_frame_restore}``. + """ + frame_save = frame_restore = spill4 = 0 + for line in asm.splitlines(): + line = line.strip() + if not line: + continue + m = re.match(r"^(sd|ld|sw|lw|fsw|flw)\s+([^,]+),\s*(-?\d+)\(sp\)", line) + if not m: + continue + op, reg, _ = m.group(1), m.group(2).strip(), int(m.group(3)) + if op in ("sd", "ld") and reg in _CALLEE_SAVED: + if op == "sd": + frame_save += 1 + else: + frame_restore += 1 + elif op in ("sw", "lw", "fsw", "flw"): + spill4 += 1 + return { + "llvm_spill_slots": spill4 // 2, + "llvm_frame_save": frame_save, + "llvm_frame_restore": frame_restore, + } + + +def _llvm_compare(cnn_path: str) -> dict: + """Compile *cnn_path* via LLVM (O2) and return comparison stats. + + Reuses the libLLVM pipeline from ``compare_codegen.py``: ONNX → + LLVM IR → RISC-V assembly at both RV64IM and RV64FD feature sets. + """ + from scratchv.standalone.compare_codegen import _load_llvm, llvm_ir_to_riscv + from scratchv.standalone.onnx_to_llvm_standalone import convert_onnx_to_llvm + from .bench_utils import llvmlite_ir_to_riscv + + # lib = _load_llvm() + ir = convert_onnx_to_llvm(cnn_path) + im_cnt, im_asm, _ = llvmlite_ir_to_riscv(ir, "+m", 2) + fd_cnt, fd_asm, fd_cats = llvmlite_ir_to_riscv(ir, "+m,+f,+d", 2) + + result = { + "llvm_im_instrs": im_cnt, + "llvm_fd_instrs": fd_cnt, + "llvm_fd_cats": fd_cats, + "llvm_fd_cat_buckets": _op_categories(fd_cats), + "_llvm_fd_asm": fd_asm, + "_llvm_im_asm": im_asm, + } + result.update(_llvm_spill_stats(fd_asm)) + return result + + # --------------------------------------------------------------------------- # Benchmark # --------------------------------------------------------------------------- @@ -197,6 +217,7 @@ def bench_allocate(cnn_path: str, phys_regs: list[str], repeats: int = 30) -> di times = [] spill_counts = [] + # Warm up for _ in range(repeats): alloc = LinearScanAllocator(phys_regs=phys_regs) t0 = time.perf_counter() @@ -210,6 +231,7 @@ def bench_allocate(cnn_path: str, phys_regs: list[str], repeats: int = 30) -> di alloc.allocate(alloc.compute_live_intervals(block)) code = alloc.get_allocated_code(block) asm_errors = _validate_asm(code) + sv_cnt, sv_cats = count_riscv_instrs(code) # Greedy allocator baseline t0 = time.perf_counter() @@ -224,10 +246,12 @@ def bench_allocate(cnn_path: str, phys_regs: list[str], repeats: int = 30) -> di "ir_inst_count": ir_count, "machine_instrs": len(machine), "vreg_count": len(alloc.alloc_map), - "spills": spill_counts[-1], "reg_spill_count": spill_counts[-1], "peak_active": alloc.peak_active, "asm_lines": len(code.splitlines()), + "sv_static_instrs": sv_cnt, + "sv_cats": sv_cats, + "sv_cat_buckets": _op_categories(sv_cats), "asm_errors": asm_errors, "asm_valid": len(asm_errors) == 0, "greedy_time_s": greedy_time, @@ -250,6 +274,18 @@ def run_bench( stats["emu_passed"] = emu["passed"] stats["emu_error"] = emu.get("error", "") stats["valid"] = stats["asm_valid"] + + # LLVM comparison (non-fatal) + try: + stats.update(_llvm_compare(cnn_path)) + stats["llvm_available"] = True + except Exception as exc: + stats["llvm_available"] = False + stats["llvm_error"] = str(exc)[:120] + if stats["llvm_available"]: + stats["instr_ratio_fd"] = round( + stats["llvm_fd_instrs"] / max(stats["sv_static_instrs"], 1), 2 + ) return stats @@ -280,7 +316,7 @@ def main(): phys_regs = list(_INT_REGS) print("=" * 60) - print("Benchmark 3 — CNN Model Integration") + print("Benchmark 3 — CNN Model Integration And Comparation With LLVM Backend") print(f" Model: {os.path.basename(args.cnn_path)}") print("=" * 60) @@ -294,7 +330,7 @@ def main(): print( f"{'cnn':>8} {stats['mean_s'] * 1000:>10.3f} " f"{stats['stdev_s'] * 1000:>10.3f} " - f"{stats['vreg_count']:>6} {stats['spills']:>7} " + f"{stats['vreg_count']:>6} {stats['reg_spill_count']:>7} " f"{stats['peak_active']:>6} {stats['asm_lines']:>5}" ) @@ -313,10 +349,33 @@ def main(): else: print(f" Emulator: ✓ passed") + # LLVM comparison output + print() + print("-" * 55) + print(" LLVM comparison (O2, same ONNX model)") + print("-" * 55) + + print( + f" ScratchV LinearScan: {stats['sv_static_instrs']} instrs " + f"{stats['sv_cat_buckets']}" + ) + print(f" LLVM RV64IM: {stats['llvm_im_instrs']} instrs") + print( + f" LLVM RV64FD: {stats['llvm_fd_instrs']} instrs " + f"({stats['instr_ratio_fd']}x vs ScratchV) " + f"{stats['llvm_fd_cat_buckets']}" + ) + print( + f" Spill (LLVM approx): {stats['llvm_spill_slots']} slots " + f"(frame save/restore {stats['llvm_frame_save']}/" + f"{stats['llvm_frame_restore']}); " + f"ScratchV (exact): reg_spill_count={stats['reg_spill_count']}" + ) + asm_ok = "PASS" if stats["asm_valid"] else "FAIL" print( f"\n asm_valid={stats['asm_valid']}, " - f"reg_spill_count={stats['spills']} [{asm_ok}]" + f"reg_spill_count={stats['reg_spill_count']} [{asm_ok}]" ) return 0 if stats["asm_valid"] else 1 diff --git a/benchmarks/test_regalloc/bench_dense.py b/benchmarks/test_regalloc/bench_dense.py index 87a1812..0be539c 100644 --- a/benchmarks/test_regalloc/bench_dense.py +++ b/benchmarks/test_regalloc/bench_dense.py @@ -110,7 +110,7 @@ def main(): "--repeats", type=int, default=30, help="Number of repeat measurements" ) args = parser.parse_args() - # limit to 5 phys_regs + phys_regs = [f"r{i}" for i in range(5)] print("=" * 60) diff --git a/benchmarks/test_regalloc/bench_regalloc_linear.py b/benchmarks/test_regalloc/bench_regalloc_linear.py index ffd4b36..003f21f 100644 --- a/benchmarks/test_regalloc/bench_regalloc_linear.py +++ b/benchmarks/test_regalloc/bench_regalloc_linear.py @@ -129,7 +129,7 @@ def main(): f"{'✓' if r2.get('valid') else '✗'}" ) - # Benchmark 3 — CNN Integration + # Benchmark 3 — CNN Integration And Comparation With LLVM cnn_default = "models/graph/cnn.onnx" r3 = bench_cnn.run_bench(cnn_path=cnn_default, repeats=args.repeats) results["3. CNN Integration"] = r3 @@ -180,4 +180,3 @@ def main(): if __name__ == "__main__": sys.exit(main() or 0) - diff --git a/benchmarks/test_regalloc/bench_simple.py b/benchmarks/test_regalloc/bench_simple.py index abfadff..a3e02e8 100644 --- a/benchmarks/test_regalloc/bench_simple.py +++ b/benchmarks/test_regalloc/bench_simple.py @@ -134,4 +134,3 @@ def main(): if __name__ == "__main__": sys.exit(main() or 0) - diff --git a/benchmarks/test_regalloc/bench_utils.py b/benchmarks/test_regalloc/bench_utils.py new file mode 100644 index 0000000..da7e631 --- /dev/null +++ b/benchmarks/test_regalloc/bench_utils.py @@ -0,0 +1,263 @@ +# flake8: noqa +"""Common used benchmark utils for all benchmarks""" + +from tabulate import tabulate + + +# Common used constants +_CALLEE_SAVED = { + "ra", + "fp", + "s0", + "s1", + "s2", + "s3", + "s4", + "s5", + "s6", + "s7", + "s8", + "s9", + "s10", + "s11", + "fs0", + "fs1", + "fs2", + "fs3", + "fs4", + "fs5", + "fs6", + "fs7", + "fs8", + "fs9", + "fs10", + "fs11", +} + +_KNOWN_OPS = { + "add", + "addi", + "sub", + "mul", + "div", + "rem", + "and", + "andi", + "or", + "ori", + "xor", + "xori", + "sll", + "slli", + "srl", + "srli", + "sra", + "srai", + "slt", + "slti", + "sltu", + "sltiu", + "lw", + "lh", + "lb", + "lbu", + "lhu", + "sw", + "sh", + "sb", + "beq", + "bne", + "blt", + "bge", + "bltu", + "bgeu", + "jal", + "jalr", + "auipc", + "lui", + "li", + "mv", + "nop", + "ret", + "bnez", + "j", + "max", + ".label", + ".text", + ".data", + ".global", + ".type", + "flw", + "fsw", + "fadd.s", + "fsub.s", + "fmul.s", + "fdiv.s", + "feq.s", + "flt.s", + "fle.s", + "fcvt.w.s", + "fcvt.s.w", +} + +# Instruction category buckets used to compare opcode mixes between the +# ScratchV backend and the LLVM backend. `sd`/`ld` are ABI stack +# save/restore pairs — the classic LLVM frame-management cost. +_CAT_ALU = { + "add", + "addi", + "sub", + "sll", + "slli", + "srl", + "srli", + "sra", + "srai", + "and", + "andi", + "or", + "ori", + "xor", + "xori", + "slt", + "slti", + "sltu", + "sltiu", + "lui", + "auipc", +} +_CAT_LOAD = {"lw", "lh", "lb", "lbu", "lhu", "flw"} +_CAT_STORE = {"sw", "sh", "sb", "fsw"} +_CAT_BRANCH = {"beq", "bne", "blt", "bge", "bltu", "bgeu", "jal", "jalr", "j"} +_CAT_MUL = {"mul", "mulh", "mulhu", "mulhsu"} +_CAT_STACK = {"sd", "ld"} + + +def _op_categories(cats: dict[str, int]) -> dict[str, int]: + """Group raw opcode counts into instruction categories.""" + out = { + "ALU": 0, + "Load": 0, + "Store": 0, + "Branch": 0, + "Mul": 0, + "Stack": 0, + "Other": 0, + } + for op, n in cats.items(): + if op in _CAT_STACK: + out["Stack"] += n + elif op in _CAT_ALU: + out["ALU"] += n + elif op in _CAT_LOAD: + out["Load"] += n + elif op in _CAT_STORE: + out["Store"] += n + elif op in _CAT_BRANCH: + out["Branch"] += n + elif op in _CAT_MUL: + out["Mul"] += n + else: + out["Other"] += n + return out + + +# RV64 ABI callee-saved registers — `sd`/`ld` to these at sp offsets are +# prologue/epilogue frame save/restore, not spills. +_CALLEE_SAVED = { + "ra", + "fp", + "s0", + "s1", + "s2", + "s3", + "s4", + "s5", + "s6", + "s7", + "s8", + "s9", + "s10", + "s11", + "fs0", + "fs1", + "fs2", + "fs3", + "fs4", + "fs5", + "fs6", + "fs7", + "fs8", + "fs9", + "fs10", + "fs11", +} + + +_llvmlite_ready = False + + +def llvmlite_ir_to_riscv( + ir_text: str, + features: str = "", + opt_level: int = 2, + output_path: str = "", +) -> tuple[int, str, dict[str, int]]: + """Compile LLVM IR → RISC-V assembly via llvmlite. + + Drop-in replacement for ``compare_codegen.llvm_ir_to_riscv``, using + llvmlite's Python bindings instead of ctypes. No ``lib`` parameter is + needed — llvmlite manages the LLVM library internally. + + Attempts to load the system ``libLLVM-20.so`` so that codegen output is + byte-identical to the existing ctypes path. Falls back to llvmlite's + bundled LLVM if the system library is not available. + + Args: + ir_text: LLVM IR source text. + features: Target features, e.g. ``""`` or ``"+f,+d"``. + opt_level: 0=None, 1=Less, 2=Default, 3=Aggressive. + output_path: If set, save assembly to this file. + + Returns: + ``(instruction_count, assembly_text, opcode_breakdown)``. + """ + global _llvmlite_ready + if not _llvmlite_ready: + from llvmlite import binding as llvm + + # Attempt to load the system libLLVM so that load_library_permanently + # can resolve any additional LLVM plugins from the system install. + # Note: llvmlite's codegen functions are pre-linked against its own + # bundled LLVM, so minor output differences (+2 instrs) from LLVM + # version evolution are expected. + try: + llvm.load_library_permanently("libLLVM-20.so") + except Exception: + pass + llvm.initialize_all_targets() + llvm.initialize_all_asmprinters() + _llvmlite_ready = True + + from llvmlite import binding as llvm + from scratchv.standalone.compare_codegen import count_riscv_instrs + + mod = llvm.parse_assembly(ir_text) + + target = llvm.Target.from_triple("riscv64-unknown-elf") + tm = target.create_target_machine( + cpu="generic-rv64", + features=features, + opt=opt_level, + reloc="default", + codemodel="default", + ) + + asm = tm.emit_assembly(mod) + + if output_path: + with open(output_path, "w") as f: + f.write(asm) + + count, cats = count_riscv_instrs(asm) + return count, asm, cats From 19bbcd4ed71daeb05e685ddbdb456e007eebfdca Mon Sep 17 00:00:00 2001 From: KangjieZhang Date: Mon, 10 Aug 2026 01:16:16 +0800 Subject: [PATCH 3/3] Add benchmark Docs --- benchmarks/test_regalloc/regalloc.md | 295 +++++++++++++++++++++++++++ 1 file changed, 295 insertions(+) create mode 100644 benchmarks/test_regalloc/regalloc.md diff --git a/benchmarks/test_regalloc/regalloc.md b/benchmarks/test_regalloc/regalloc.md new file mode 100644 index 0000000..730e4dc --- /dev/null +++ b/benchmarks/test_regalloc/regalloc.md @@ -0,0 +1,295 @@ +# 寄存器分配 Benchmark 设计文档 + +## 1. 概述 + +对 ScratchV 的**线性扫描寄存器分配器**(`LinearScanAllocator`)进行正确性与性能评估,并在 CNN 路径上与 LLVM 后端做指令数及寄存器溢出的交叉对比。 + +**设计目标**: + +- **正确性**:验证无溢出 / 有溢出两种场景的分配结果合法(无未解析 vreg、有效 opcode) +- **性能**:测量分配耗时(均值 / 标准差)、活跃区间峰值压力 +- **可对比**:每项输出统一的 `reg_spill_count` 指标,支持回归对比和筛选 +- **跨后端对比**:同一 ONNX 模型经 ScratchV 和 LLVM 两条路径编译,对比静态指令数、opcode 类别分布、溢出/帧操作数量 + +入口:`benchmarks/test_regalloc/run_benchmark.py` +单文件运行:直接执行 `bench_simple.py` / `bench_dense.py` / `bench_cnn.py` + +--- + +## 2. 架构 + +``` +benchmarks/test_regalloc/ +├── __init__.py +├── bench_utils.py 共享常量(opcode 分类、callee-saved 集合)、llvmlite IR→RISC-V +├── bench_simple.py Benchmark 1 — 无溢出正确性 +├── bench_dense.py Benchmark 2 — 溢出正确性 +├── bench_cnn.py Benchmark 3 — CNN 集成 + LLVM 对比 +└── run_benchmark.py 运行器:汇总 3 路输出 → JSON / HTML / Markdown 报告 +``` + +### 2.1 统一接口 + +每个 benchmark 文件导出: + +```python +def run_bench(...) -> dict: + """返回统一结构的统计 dict,必需键见 §4。""" +``` + +运行器遍历三个 `run_bench()`,收集 dict 生成报告。 + +### 2.2 数据流 + +``` + ┌──────────────────┐ + │ run_benchmark.py │ + └──────┬───────────┘ + ┌─────────────┼─────────────┐ + ▼ ▼ ▼ + bench_simple bench_dense bench_cnn + run_bench() run_bench() run_bench() + │ │ │ + ▼ ▼ ▼ + LinearScanAllocator ... LinearScanAllocator + .allocate() .allocate() + .report() / to_dict() .report() / to_dict() + │ │ │ │ + ▼ ▼ ▼ ▼ + {dict} {dict} {dict} {LLVM dict} + │ │ │ │ + └──────────────┴─────────┴─────────┘ + │ + ▼ + JSON / HTML / MD 报告 +``` + +--- + +## 3. 三项 Benchmark + +### 3.1 Benchmark 1 — 简单算术(无溢出) + +**文件**:`bench_simple.py` +**目的**:验证物理寄存器充足时分配器产生零溢出。 + +| 参数 | 值 | +|------|-----| +| 虚拟寄存器 | 5 个(`v0`–`v4`) | +| 物理寄存器池 | 8 个(`r0`–`r7`) | +| 指令数 | 10 条(随机 add/sub/mul/and/or) | +| 块生成 | `_gen_block(num_insts=10, num_vregs=5)` | +| 断言 | `reg_spill_count == 0` → `valid = True` | + + + +### 3.2 Benchmark 2 — 密集计算(触发溢出) + +**文件**:`bench_dense.py` +**目的**:人为制造高寄存器压力,迫使分配器溢出,验证溢出代码生成正确。 + +| 参数 | 值 | +|------|-----| +| 虚拟寄存器 | 30 个(`v0`–`v29`) | +| 物理寄存器池 | 5 个(`r0`–`r4`) | +| 指令数 | 80 条(30 条 define + 50 条交叉引用) | +| 块生成 | 阶段 1: 逐个定义 vreg → 创建长 live range;阶段 2: 随机交叉引用保持活跃 | +| 断言 | `reg_spill_count > 0` → `valid = True` | + + + +### 3.3 Benchmark 3 — CNN 集成 + LLVM 对比 + +**文件**:`bench_cnn.py` +**目的**:通过真实 ONNX 模型编译流水线验证分配器,并与 LLVM 后端做指令数和溢出对比。 + +**ScratchV 编译流水线**: + +``` +ONNXParser → IR Program (17 ops) + → ConstantFolder → DeadCodeEliminator + → InstructionSelector → MachineInstr (57, with vregs) + → block_from_machine_instrs → [LsInstruction] + → LinearScanAllocator.allocate() + → get_allocated_code() → RISC-V 伪指令 + → _validate_asm() # 检查无未解析 vreg、合法 opcode +``` + +**LLVM 对比流水线**: + +``` +convert_onnx_to_llvm(model) → LLVM IR (866K lines, 183MB) + → llvmlite IR → RISC-V asm → 静态指令数 + opcode 分布 + → 汇编启发式 → 溢出 slot / 帧操作 近似统计 +``` + +| 参数 | 值 | +|------|-----| +| 模型 | `models/graph/cnn.onnx`(可 CLI 覆盖) | +| IR 指令 | 17 条(3×conv + 3×relu + 3×maxpool + 2×gemm + sigmoid + 2×reshape) | +| 物理寄存器 | `_INT_REGS`(28 个) | +| ScratchV 输出 | ~57 条伪指令(mv/mul/add/slt/bnez…) | +| LLVM 输出 | ~1099 条(RV64FD O2,真实循环展开) | +| 断言 | `asm_valid == True` | + +> **注意**:ScratchV 侧输出 57 条**伪指令**(conv/maxpool 等语义级操作由仿真器实现),LLVM 侧输出 1099 条**自包含机器指令**(每个 conv 展开为 5 重嵌套循环的完整 RISC-V 指令序列)。`instr_ratio_fd ≈ 23.89x` 反映的是抽象层级差异而非优化能力差距,因此还提供 opcode **类别分布**作为跨层级可比指标。 + +--- + +## 4. 指标规范 + +### 4.1 所有 benchmark 通用键 + +| 键 | 类型 | 来源 | 说明 | +|----|------|------|------| +| `mean_s` | `float` | `perf_counter` 均值 | 单次分配耗时(秒) | +| `stdev_s` | `float` | `stdev` | 耗时标准差 | +| `vreg_count` | `int` | `len(alloc.alloc_map)` | 已分配的虚拟寄存器数 | +| `spills` | `int` | `len(alloc._spill_slots)` | 溢出 slot 数(别名) | +| `reg_spill_count` | `int` | 同上 | **统一溢出指标键**(接口规范) | +| `peak_active` | `int` | `alloc.peak_active` | 峰值同时活跃的物理寄存器数 | +| `asm_lines` | `int` | `len(code.splitlines())` | 汇编输出行数 | +| `valid` | `bool` | 由 `run_bench()` 设置 | 该项是否通过断言 | + +### 4.2 Benchmark 特有键 + +**bench_dense**: + +| 键 | 说明 | +|----|------| +| `reloads` | `lw ... # reload` 注释行数 | + +**bench_cnn (ScratchV 侧)**: + +| 键 | 来源 | 说明 | +|----|------|------| +| `vreg_total` | ONNXParser→isel | 编译流水线中出现的 vreg 总数 | +| `ir_inst_count` | Program 指令计数 | IR 层操作数(=17) | +| `machine_instrs` | `len(machine)` | MachineInstr 数量(=57) | +| `sv_static_instrs` | `count_riscv_instrs()` | 汇编指令数(~46,不含标签/注释) | +| `sv_cats` | `count_riscv_instrs()` | opcode 原始计数 | +| `sv_cat_buckets` | `_op_categories()` | 6 类汇总(ALU/Load/Store/Branch/Mul/Other) | +| `asm_errors` | `_validate_asm()` | 未解析 vreg / 未知 opcode 列表 | +| `asm_valid` | `len(asm_errors) == 0` | 汇编合法性 | +| `greedy_time_s` | Greedy allocator | Greedy分配器耗时(baseline) | +| `greedy_out_instrs` | — | Greedy分配器输出指令数 | + +**bench_cnn (LLVM 对比侧)**: + +| 键 | 说明 | +|----|------| +| `llvm_im_instrs` | LLVM RV64IM 静态指令数 | +| `llvm_fd_instrs` | LLVM RV64FD 静态指令数 | +| `instr_ratio_fd` | `llvm_fd_instrs / max(sv_static_instrs, 1)` | +| `llvm_fd_cats` | opcode 原始计数 | +| `llvm_fd_cat_buckets` | 7 类汇总(+ Stack) | +| `llvm_spill_slots` | 4 字节 sp 访问 ÷ 2(**近似**) | +| `llvm_frame_save` | `sd` 到 callee-saved 的计数 | +| `llvm_frame_restore` | `ld` 到 callee-saved 的计数 | + +### 4.3 `reg_spill_count` 规范 + +- **经过 regalloc 的路径**:直接取自 `alloc._spill_slots` 长度 → 精确值 +- **不经过 regalloc 的路径**:LLVM 侧 `reg_spill_count` 是本路径的 ScratchV 精确值(0);LLVM 近似溢出独立为 `llvm_spill_slots`,不污染统一键 +- **降级路径**:当 libLLVM 不可用时,`llvm_fd_instrs`/`llvm_spill_slots` 等键不存在于 dict 中,报告渲染 fallback 到 `"-"` + +--- + +## 5. LLVM 溢出统计 + +由于 libLLVM 缺少 regalloc pass 统计入口,LLVM 侧无法获取精确的寄存器溢出计数,通过汇编层面识别特定模式统计溢出: + +| 形态 | 正则 / 判定 | 含义 | +|------|------------|------| +| 帧保存 | `sd , N(sp)` | prologue 保存 callee-saved 寄存器 | +| 帧恢复 | `ld , N(sp)` | epilogue 恢复 callee-saved 寄存器 | +| 溢出 | `sw/lw/fsw/flw , N(sp)` | 寄存器值被 spill→reload(4 字节) | + +**callee-saved 集合**(RV64 ABI):`ra, fp, s0–s11, fs0–fs11` + +--- + +## 6. 输出格式 + +### 6.1 终端 + +``` +============================================================ + ScratchV — Register Allocation Benchmark Suite +============================================================ + 1. Simple: reg_spill_count=0, mean=0.019ms ✓ + 2. Dense: reg_spill_count=15, mean=0.084ms ✓ + 3. CNN: reg_spill_count=0, mean=0.052ms ✓ + LLVM: RV64FD=1099 instrs (23.89x vs ScratchV 46) + + Total: 25173.6ms PASS + + JSON report: /tmp/rg_spill.json + HTML report: /tmp/rg_spill.html + Markdown: /tmp/rg_spill.md +``` + +### 6.2 JSON + +标准化结构,`_` 前缀字段(如 asm 文本、allocator 实例)被排除: + +```json +{ + "timestamp": "2026-08-09T...", + "total_time_s": 25.17, + "repeats": 3, + "results": { + "1. Simple Arithmetic": { + "mean_s": 1.9e-05, "reg_spill_count": 0, "peak_active": 5, + "asm_lines": 10, "valid": true + }, + "2. Dense Computation": { + "mean_s": 8.4e-05, "reg_spill_count": 15, "peak_active": 14, + "asm_lines": 179, "reloads": 61, "valid": true + }, + "3. CNN Integration": { + "mean_s": 5.2e-05, "reg_spill_count": 0, + "sv_static_instrs": 46, "llvm_fd_instrs": 1099, + "instr_ratio_fd": 23.89, "llvm_spill_slots": 87, + "llvm_frame_save": 70, "llvm_frame_restore": 70, + "llvm_fd_cat_buckets": {"ALU": 346, "Load": 98, ...}, + "asm_valid": true, "valid": true + } + } +} +``` + +### 6.3 HTML / Markdown 汇总表 + +| Benchmark | Mean(ms) | Std(ms) | Vregs | Spills | Peak | Reloads | Asm | LLVM-FD | Ratio | LLVM-Spill | Valid | +|-----------|----------|---------|-------|--------|------|---------|-----|---------|-------|------------|-------| +| 1. Simple Arithmetic | 0.019 | 0.011 | 5 | 0 | 5 | - | 10 | - | - | - | ✓ | +| 2. Dense Computation | 0.084 | 0.016 | 30 | 15 | 14 | 61 | 179 | - | - | - | ✓ | +| 3. CNN Integration | 0.052 | 0.015 | 30 | 0 | 11 | - | 57 | 1099 | 23.89 | 87 | ✓ | + +--- + +## 7. 使用方式 + +```bash +# 运行全部 3 项 benchmark,生成三格式报告 +python benchmarks/test_regalloc/run_benchmark.py \ + --repeats 30 \ + --output-json report.json \ + --output-html report.html \ + --output-md report.md + +# 单独运行某项 +python benchmarks/test_regalloc/bench_simple.py --repeats 100 +python benchmarks/test_regalloc/bench_dense.py --repeats 50 +python benchmarks/test_regalloc/bench_cnn.py \ + --cnn-path models/graph/cnn.onnx --repeats 30 +``` + +--- + +## 8. TODO + +1. **优化报告输出格式** +2. **将通用的Benchmark组件进一步抽象到`bench_utils.py`当中** +3. **目前ONNX模型的编译路径指令选择方面无法完全正确生成算子的汇编指令,与LLVM后端对比不公平**