diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 365a2d9..9230ca8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,6 +13,50 @@ permissions: contents: read jobs: + scheduler-review: + runs-on: ubuntu-24.04 + timeout-minutes: 15 + env: + SCRATCHV_REQUIRE_RISCV_EXECUTION: "1" + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + with: + python-version: "3.12" + - name: Install RISC-V execution and independent scheduling tools + run: | + sudo apt-get update + sudo apt-get install --no-install-recommends -y clang-18 lld-18 llvm-18 qemu-user + echo "/usr/lib/llvm-18/bin" >> "$GITHUB_PATH" + python -m pip install -e '.[riscv]' 'pytest>=7,<10' + - name: Require real RISC-V execution regressions + run: | + command -v clang + command -v ld.lld + command -v qemu-riscv32 + mkdir -p benchmark_reports + python -m pytest tests/test_inst_scheduler*.py tests/test_regalloc_linear.py tests/test_pr37_regression.py \ + -q --junit-xml=benchmark_reports/scheduler_tests.xml + - name: Generate CNN input and audit both CPU models + run: | + mkdir -p models/graph + python scripts/gen_minimal_cnn.py + python -m benchmarks.audit_inst_scheduler --llvm-mca llvm-mca-18 + - name: Publish scheduling audit summary + if: always() + run: | + if [ -f benchmark_reports/inst_scheduler_audit.md ]; then + cat benchmark_reports/inst_scheduler_audit.md >> "$GITHUB_STEP_SUMMARY" + fi + - name: Upload scheduling verification evidence + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: scheduler-review-report + path: benchmark_reports/ + # ═════════════════════════════════════════════════════════════════════════ # 课题功能测试:所有14个模块的单元测试 + 集成测试 # ═════════════════════════════════════════════════════════════════════════ @@ -258,6 +302,19 @@ jobs: --json benchmark_reports/const_merge_report.json \ --markdown benchmark_reports/const_merge_report.md + # ── 课题18:固定用例执行验证与独立合成规模测试 ───────────────── + - name: Topic 18 scheduling case report + run: | + $PY -m benchmarks.run_inst_scheduler_case \ + --json benchmark_reports/inst_scheduler_report.json \ + --markdown benchmark_reports/inst_scheduler_report.md + + - name: Topic 18 synthetic scheduling benchmark + run: | + $PY -m benchmarks.bench_inst_scheduler --repeats 3 \ + --json benchmark_reports/inst_scheduler_synthetic.json \ + --markdown benchmark_reports/inst_scheduler_synthetic.md + # ── 3.1.2 课题13:窥孔前后对比报告 ───────────────────────────── - name: Topic 13 peephole compare report run: | @@ -289,6 +346,13 @@ jobs: --asm benchmark_reports/cnn_scratchv.s \ --estimate --report --const-merge + - name: Topic 18 CNN assembly scheduling A/B + run: | + $PY -m benchmarks.run_inst_scheduler_case \ + benchmark_reports/cnn_scratchv.s --static-only \ + --json benchmark_reports/inst_scheduler_cnn.json \ + --markdown benchmark_reports/inst_scheduler_cnn.md + # ── 3.3.1 汇编美化器:真实 CNN 输出 + Actions Summary ─────────── - name: CNN assembly beautifier benchmark run: | @@ -411,6 +475,11 @@ jobs: if [ -f benchmark_reports/const_merge_report.md ]; then cat benchmark_reports/const_merge_report.md >> $GITHUB_STEP_SUMMARY fi + for report in inst_scheduler_report inst_scheduler_cnn inst_scheduler_synthetic; do + if [ -f "benchmark_reports/$report.md" ]; then + cat "benchmark_reports/$report.md" >> "$GITHUB_STEP_SUMMARY" + fi + done echo "" >> $GITHUB_STEP_SUMMARY if [ -f benchmark_reports/peephole_compare.md ]; then echo "### Topic 13 Peephole Compare" >> $GITHUB_STEP_SUMMARY diff --git a/.gitignore b/.gitignore index 0af9cbc..d8ad9e7 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,6 @@ benchmark_reports/ *.dot *.png .idea/ + +# Local tool session state +/:memory:.ses diff --git a/CHANGELOG.md b/CHANGELOG.md index a98014e..0ad3309 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## Unreleased — Topic 18 review iteration (2026-09-16) + +- Keep instruction scheduling disabled by default at the assembly-text V1 entry point. Model cycles and llvm-mca estimates are not hardware speedups or dynamic-instruction-count improvements. +- Use conservative integer latencies, preserve division order, account for write completion, and require gains under a second latency table before applying a candidate. +- Restore linear-allocator branch operands and labels; classify branch/store operands as reads and emit canonical integer zero-register sources. +- Report coverage, unsupported opcodes and latency-sensitive rejections; restore section-stack state and distinguish quoted text from layout directives. +- Add a required RISC-V execution CI job and independent two-CPU llvm-mca/real-input audit reports. +- API migration: `SchedInst` is immutable and recomputes def/use; `build_dag` rejects multiple regions; `machine_instrs_from_scheduled` rejects lossy conversions. Use `schedule_assembly` for complete assembly, and read the compiler report from `stats["schedule"]["report"]` instead of `warnings`. +- Structured post-RA/pre-emission scheduling and Fast/BURR strategy selection remain future work; see `docs/topic-18/README.md`. + ## [0.3.0] — 2026-05-18 ### Added diff --git a/benchmarks/audit_inst_scheduler.py b/benchmarks/audit_inst_scheduler.py new file mode 100644 index 0000000..39abd27 --- /dev/null +++ b/benchmarks/audit_inst_scheduler.py @@ -0,0 +1,224 @@ +"""Audit real compiler coverage and independent llvm-mca scheduling A/B results. + +Run from the repository root. llvm-mca is required: missing tools, compilation +failures and unsupported MCA inputs are errors, never silently skipped cases. +MCA cycles are target-model estimates, not hardware measurements. +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import re +import shutil +import subprocess +from pathlib import Path + +from benchmarks.bench_inst_scheduler import _gen_instructions +from scratchv.backend.inst_scheduler import ScheduleConfig, parse_instructions, schedule_assembly +from scratchv.backend.schedule_semantics import memory_address +from scratchv.compiler import CompilerConfig, CompilerDriver + +CPUS = ("rocket-rv32", "sifive-e76") +SIZES = (10, 50, 100, 200, 500, 1000) +SEEDS = (42, 1, 2) +CHAINS = (1, 2, 3, 8) + + +def mca_cycles(executable: str, cpu: str, source: str) -> int: + result = subprocess.run( + [executable, "-mtriple=riscv32", f"-mcpu={cpu}", "-mattr=+m,+f,+d", "-iterations=1"], + input=source, text=True, capture_output=True, timeout=30, check=True, + ) + # Never accept partial analysis (e.g. an ignored unsupported instruction). + if result.stderr.strip(): + raise RuntimeError(f"llvm-mca diagnostic for {cpu}: {result.stderr.strip()}") + match = re.search(r"^Total Cycles:\s*(\d+)$", result.stdout, re.MULTILINE) + if not match: + raise RuntimeError(f"llvm-mca did not report Total Cycles for {cpu}") + return int(match.group(1)) + + +def compare(executable: str, before: str, after: str) -> dict: + results = {} + for cpu in CPUS: + original = mca_cycles(executable, cpu, before) + final = original if before == after else mca_cycles(executable, cpu, after) + results[cpu] = {"before": original, "after": final, "saved": original - final} + return results + + +def summarize(rows: list[dict]) -> dict: + summary = {} + for cpu in CPUS: + samples = [row["mca"][cpu] for row in rows] + summary[cpu] = { + "samples": len(samples), + "before": sum(row["before"] for row in samples), + "after": sum(row["after"] for row in samples), + "saved": sum(row["saved"] for row in samples), + "win": sum(row["saved"] > 0 for row in samples), + "tie": sum(row["saved"] == 0 for row in samples), + "loss": sum(row["saved"] < 0 for row in samples), + } + return summary + + +def canonical_region(source: str, start: int, end: int) -> str: + """Render modeled body instructions in GAS syntax for independent MCA. + + Branches stay fixed and are excluded from these local body estimates; + source line boundaries and original source hashes remain in the report. + """ + lines = [] + for inst in parse_instructions(source): + if not start <= inst.id + 1 <= end or inst.terminator: + continue + if inst.effects.barrier_reason: + raise ValueError(f"unmodeled instruction in MCA region: {inst.raw_line}") + operands = list(inst.operands) + address = inst.effects.address + if address is not None: + reg = operands[1] if memory_address(operands[0]) else operands[0] + operands = [reg, f"{address.offset}({address.base})"] + lines.append(inst.opcode + " " + ", ".join(operands)) + return "\n".join(lines) + "\n" + + +def run_audit(executable: str, root: Path) -> dict: + version = subprocess.run([executable, "--version"], text=True, capture_output=True, + timeout=10, check=True).stdout.splitlines()[:3] + synthetic = [] + for size in SIZES: + for seed in SEEDS: + for chains in CHAINS: + source = "\n".join(i.raw_line for i in _gen_instructions(size, seed, chains)) + "\n" + result = schedule_assembly(source, ScheduleConfig(strict=True)) + synthetic.append({ + "size": size, "seed": seed, "chains": chains, + "input_sha256": hashlib.sha256(source.encode()).hexdigest(), + "changed": result.changed, + "model_before": result.stats["original_cycles"], + "model_after": result.stats["final_cycles"], + "sensitivity_rejected": result.stats.get("sensitivity_rejected_regions", 0), + "mca": compare(executable, source, result.asm_text), + }) + + paths = sorted((root / "benchmarks/cases").glob("[0-9]*.dsl")) + cnn = root / "models/graph/cnn.onnx" + if not cnn.exists(): + raise FileNotFoundError(f"real-input audit requires {cnn}; run scripts/gen_minimal_cnn.py") + paths.append(cnn) + real = [] + real_regions = [] + for allocator in ("greedy", "linear"): + driver = CompilerDriver(CompilerConfig(reg_alloc=allocator, schedule=True, schedule_strict=True)) + for path in paths: + source = driver._generate_code(driver._parse(str(path))) + stats, warnings = {}, [] + final = driver._run_asm_passes(source, warnings, stats) + schedule = stats["schedule"] + name = path.relative_to(root).as_posix() + real.append({ + "input": name, "allocator": allocator, + "input_file_sha256": hashlib.sha256(path.read_bytes()).hexdigest(), + "input_sha256": hashlib.sha256(source.encode()).hexdigest(), + "output_sha256": hashlib.sha256(final.encode()).hexdigest(), + "changed": source != final, "scheduling": schedule, + "source_instructions_before": len(parse_instructions(source)), + "source_instructions_after": len(parse_instructions(final)), + "execution_verified": False, + }) + for region in schedule["regions"]: + if region["status"] != "applied": + continue + start, end = region["start_line"], region["end_line"] + before = canonical_region(source, start, end) + after = canonical_region(final, start, end) + real_regions.append({ + "input": name, "allocator": allocator, "start_line": start, "end_line": end, + "scope": "applied region body; fixed terminators excluded", + "before": before, "after": after, "mca": compare(executable, before, after), + }) + + coverage = {} + for allocator in ("greedy", "linear"): + rows = [r for r in real if r["allocator"] == allocator] + total = sum(r["scheduling"]["input_instructions"] for r in rows) + modeled = sum(r["scheduling"]["modeled_instructions"] for r in rows) + sizes = [region["instructions"] for r in rows for region in r["scheduling"]["regions"]] + coverage[allocator] = { + "files": len(rows), "changed_files": sum(r["changed"] for r in rows), + "input_instructions": total, "modeled_instructions": modeled, + "unmodeled_instructions": total - modeled, + "coverage_ratio": modeled / total if total else 0.0, + "mean_region_size": sum(sizes) / len(sizes) if sizes else 0.0, + "max_region_size": max(sizes, default=0), + "instruction_count_preserved": all( + r["source_instructions_before"] == r["source_instructions_after"] + for r in rows + ), + } + synthetic_summary = summarize(synthetic) + real_summary = summarize(real_regions) + # The review asks for positive aggregate savings and at least as many wins + # as losses. Every sample, including losses and ties, remains in the JSON. + passed = all(s["saved"] > 0 and s["win"] >= s["loss"] for s in synthetic_summary.values()) + passed &= all(s["saved"] >= 0 and s["win"] >= s["loss"] for s in real_summary.values()) + passed &= all(s["instruction_count_preserved"] for s in coverage.values()) + return { + "benchmark_type": "scheduler-review-audit", "status": "passed" if passed else "failed", + "llvm_mca_version": version, "iterations": 1, "cpus": list(CPUS), + "scope": "LLVM static CPU models; no hardware timing or whole-program execution", + "model": ScheduleConfig().model.name, + "synthetic_summary": synthetic_summary, "real_summary": real_summary, + "coverage": coverage, "synthetic": synthetic, "real": real, "real_regions": real_regions, + } + + +def markdown(report: dict) -> str: + lines = [ + "# Topic 18:Review 迭代独立验证", "", + f"验收:**{report['status']}**;调度模型:`{report['model']}`。", + "llvm-mca:" + "; ".join(report["llvm_mca_version"]) + "。", + "每个样例运行 1 次迭代;这是 LLVM CPU 模型估算,不是硬件计时。", "", + "| 语料 | CPU | 原序周期 | 最终周期 | 节省 | 胜 / 平 / 负 |", + "|---|---|---:|---:|---:|---|", + ] + for key, label in (("synthetic_summary", "72 个合成样例"), ("real_summary", "真实产物已应用区域")): + for cpu, row in report[key].items(): + lines.append(f"| {label} | {cpu} | {row['before']} | {row['after']} | {row['saved']} | " + f"{row['win']} / {row['tie']} / {row['loss']} |") + lines += ["", "真实产物按已应用区域的指令体估算,排除固定终止指令;未变化文件也保留在 JSON 中。", "", + "| 分配器 | 变化文件 / 总文件 | 建模 / 输入指令 | 覆盖率 | 平均 / 最大区域长度 |", + "|---|---:|---:|---:|---:|"] + for allocator, row in report["coverage"].items(): + lines.append(f"| {allocator} | {row['changed_files']} / {row['files']} | " + f"{row['modeled_instructions']} / {row['input_instructions']} | " + f"{row['coverage_ratio']:.1%} | {row['mean_region_size']:.2f} / {row['max_region_size']} |") + lines += ["", "合成验收要求每个 CPU 总节省 > 0 且胜例数 ≥ 负例数;真实已应用区域要求总节省 ≥ 0 且胜例数 ≥ 负例数。", + "通过总体门槛不代表每个样例都加速。完整 JSON 保留负例、零收益、未建模原因和输入哈希。", ""] + return "\n".join(lines) + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--llvm-mca", default="llvm-mca") + parser.add_argument("--json", type=Path, default=Path("benchmark_reports/inst_scheduler_audit.json")) + parser.add_argument("--markdown", type=Path, default=Path("benchmark_reports/inst_scheduler_audit.md")) + args = parser.parse_args(argv) + executable = shutil.which(args.llvm_mca) + if executable is None: + parser.error(f"llvm-mca is required: {args.llvm_mca}") + report = run_audit(executable, Path.cwd()) + args.json.parent.mkdir(parents=True, exist_ok=True) + args.markdown.parent.mkdir(parents=True, exist_ok=True) + args.json.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n") + args.markdown.write_text(markdown(report)) + print(markdown(report)) + return 0 if report["status"] == "passed" else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/benchmarks/bench_inst_scheduler.py b/benchmarks/bench_inst_scheduler.py index 7fdadfc..cd10089 100644 --- a/benchmarks/bench_inst_scheduler.py +++ b/benchmarks/bench_inst_scheduler.py @@ -1,182 +1,176 @@ -# flake8: noqa -"""Benchmark for Instruction Scheduler (List Scheduling). +"""Benchmark legal physical-register regions through the production scheduler. -Measures DAG construction time, scheduling time, and cycle count -improvement for basic blocks of varying size and dependency depth. - -Usage: - python benchmarks/bench_inst_scheduler.py - python benchmarks/bench_inst_scheduler.py --repeats 100 +Run: python -m benchmarks.bench_inst_scheduler --repeats 20 --json report.json --markdown report.md +Reported cycles are sums of local static model estimates, not CPU measurements. +These generated inputs are synthetic; compiler-output A/B and real execution +are reported separately by benchmarks.run_inst_scheduler_case. """ from __future__ import annotations import argparse +import hashlib +import json +import random import statistics import time -from typing import Optional +from pathlib import Path from scratchv.backend.inst_scheduler import ( - InstructionScheduler, SchedInst, + InstructionScheduler, + SchedInst, + ScheduleConfig, + schedule_assembly, ) -def _gen_instructions(num_insts: int, seed: int = 42, - dep_chains: int = 3) -> list[SchedInst]: - """Generate synthetic instructions with controlled dependency patterns. - - Parameters - ---------- - num_insts: - Number of instructions to generate. - seed: - Random seed. - dep_chains: - Number of dependency chains to create. - """ - import random - random.seed(seed) - - ops = ["add", "sub", "mul", "lw", "sw", "xor", "or", "and", - "addi", "slli", "srli", "div", "li", "mv"] - - # Create register groups (each chain uses its own group to avoid cross-chain deps) - reg_prefixes = [f"r{c}_" for c in range(dep_chains)] - all_regs = [] - for prefix in reg_prefixes: - all_regs.extend([f"{prefix}{i}" for i in range(max(2, num_insts // dep_chains))]) - - insts = [] - prev_rd: list[Optional[str]] = [None] * dep_chains - - for i in range(num_insts): - chain = i % dep_chains - prefix = reg_prefixes[chain] - op = random.choice(ops) - - if op in ("li", "mv"): - rd = f"{prefix}{i}" - src = prev_rd[chain] or f"{prefix}0" - insts.append(SchedInst( - id=i, opcode=op, operands=[rd, src], - defines={rd}, uses={src}, - )) - prev_rd[chain] = rd - elif op in ("lw", "sw"): - rd = f"{prefix}{i}" - addr_reg = f"{prefix}addr_{chain}" - insts.append(SchedInst( - id=i, opcode=op, operands=[rd, f"0({addr_reg})"], - defines={rd}, uses={addr_reg}, - )) - prev_rd[chain] = rd +def _gen_instructions( + num_insts: int, seed: int = 42, dep_chains: int = 3 +) -> list[SchedInst]: + """Generate real opcodes/operands; keep a0 as an unchanged memory base.""" + rng = random.Random(seed) + registers = [f"x{i}" for i in list(range(5, 10)) + list(range(11, 32))] + chain_count = max(1, min(dep_chains, len(registers))) + groups = [registers[i::chain_count] for i in range(chain_count)] + previous = [group[0] for group in groups] + instructions = [] + for index in range(num_insts): + chain = index % chain_count + dst, src = rng.choice(groups[chain]), previous[chain] + op = rng.choice(["add", "sub", "mul", "div", "lw", "sw", "addi", "li", "mv"]) + if op in {"lw", "sw"}: + operands = [dst, f"{rng.randrange(8) * 4}(a0)"] + elif op == "li": + operands = [dst, str(rng.randrange(-100, 100))] + elif op == "mv": + operands = [dst, src] + elif op == "addi": + operands = [dst, src, str(rng.randrange(-16, 16))] else: - rd = f"{prefix}{i}" - src1 = prev_rd[chain] or f"{prefix}0" - src2 = f"{prefix}{random.randint(0, max(1, i-1))}" - insts.append(SchedInst( - id=i, opcode=op, operands=[rd, src1, src2], - defines={rd}, uses={src1, src2}, - )) - prev_rd[chain] = rd - - return insts + operands = [dst, src, rng.choice(groups[chain])] + instructions.append( + SchedInst(index, op, operands, raw_line=f" {op} " + ", ".join(operands)) + ) + if op != "sw": + previous[chain] = dst + return instructions def bench_build_dag(insts: list[SchedInst], repeats: int = 20) -> dict: - """Benchmark DAG construction.""" - scheduler = InstructionScheduler() times = [] for _ in range(repeats): - t0 = time.perf_counter() - dag = scheduler.build_dag(insts) - t1 = time.perf_counter() - times.append(t1 - t0) - return { - "num_nodes": len(dag), - "mean_s": statistics.mean(times), - "stdev_s": statistics.stdev(times) if len(times) > 1 else 0, - } - - -def bench_schedule(insts: list[SchedInst], repeats: int = 20) -> dict: - """Benchmark the full scheduling pipeline.""" + start = time.perf_counter() + dag = InstructionScheduler().build_dag(insts) + times.append(time.perf_counter() - start) + return {"num_nodes": len(dag), "mean_s": statistics.mean(times)} + + +def bench_schedule( + insts: list[SchedInst], repeats: int = 20, max_region_size: int = 1024 +) -> dict: + if repeats < 1: + raise ValueError("repeats must be positive") + source = "\n".join(inst.raw_line for inst in insts) + "\n" times = [] + config = ScheduleConfig(strict=True, max_region_size=max_region_size) for _ in range(repeats): - scheduler = InstructionScheduler() - t0 = time.perf_counter() - dag = scheduler.build_dag(insts) - scheduled = scheduler.schedule(dag) - t1 = time.perf_counter() - times.append(t1 - t0) - orig_cycles = scheduler.estimate_cycles(insts) - sched_cycles = scheduler.estimate_cycles(scheduled) + start = time.perf_counter() + result = schedule_assembly(source, config) + times.append(time.perf_counter() - start) + stats = result.stats return { + "benchmark_type": "synthetic", + "input_sha256": hashlib.sha256(source.encode("utf-8")).hexdigest(), + "repeats": repeats, + "max_region_size": max_region_size, "num_insts": len(insts), - "orig_cycles": orig_cycles, - "sched_cycles": sched_cycles, - "improvement": orig_cycles - sched_cycles, + "orig_cycles": stats["original_cycles"], + "sched_cycles": stats["final_cycles"], + "improvement": stats["saved_cycles"], + "modeled": stats["modeled_instructions"], + "skipped": stats["skipped_regions"], + "moved": stats["moved_instructions"], + "applied_regions": stats["applied_regions"], + "unchanged_regions": sum( + row["status"] == "no_improvement" for row in stats["regions"] + ), + "sensitivity_rejected_regions": stats["sensitivity_rejected_regions"], + "execution_verified": False, + "model": stats["model"], "mean_s": statistics.mean(times), - "stdev_s": statistics.stdev(times) if len(times) > 1 else 0, + "stdev_s": statistics.stdev(times) if repeats > 1 else 0, } -def main(): - parser = argparse.ArgumentParser(description="Instruction Scheduler Benchmark") - parser.add_argument("--repeats", type=int, default=20, - help="Number of repeat measurements") +def _markdown(records: list[dict]) -> str: + lines = [ + "# 课题 18:合成指令调度 Benchmark", + "", + "固定随机种子生成合法物理寄存器指令,单独统计优化器耗时和局部模型收益。", + "这些数据不是实际工作负载;未执行汇编,也未测量硬件周期。", + "", + "| 指令数 | 已建模 | 原序周期(估算) | 最终周期(估算) | 减少 | 移动指令 | 应用区域 | 无收益区域 | 敏感性回退区域 | 跳过区域 | 耗时均值 ± 标准差(ms) |", + "|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|", + ] + for row in records: + modeled = row["modeled"] > 0 + before = row["orig_cycles"] if modeled else "N/A" + after = row["sched_cycles"] if modeled else "N/A" + saved = row["improvement"] if modeled else "N/A" + lines.append( + f"| {row['num_insts']} | {row['modeled']} | {before} | {after} | {saved} | " + f"{row['moved']} | {row['applied_regions']} | {row['unchanged_regions']} | " + f"{row['sensitivity_rejected_regions']} | {row['skipped']} | " + f"{row['mean_s'] * 1000:.3f} ± {row['stdev_s'] * 1000:.3f} |" + ) + if records: + row = records[0] + lines.extend( + [ + "", + f"模型:`{row['model']}`;重复次数:{row['repeats']};区域上限:{row['max_region_size']}。", + "未建模区域显示 N/A,不表示零周期;零收益与跳过样例均保留。", + "JSON 保留数组格式,逐条记录样例类型、输入 SHA-256、种子和统计口径。", + ] + ) + return "\n".join(lines) + "\n" + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--repeats", type=int, default=20) + parser.add_argument("--max-region-size", type=int, default=1024) + parser.add_argument("--json", type=Path) + parser.add_argument("--markdown", type=Path) args = parser.parse_args() - - print("=" * 80) - print("RISC-V Instruction Scheduler (List Scheduling) Benchmark") - print("=" * 80) - - # Varying instruction count - print(f"\nVarying Instruction Count (3 dependency chains):") - print("-" * 80) - print(f"{'Size':>8} {'Build(ms)':>10} {'Sched(ms)':>10} " - f"{'OrigCyc':>8} {'SchedCyc':>8} {'Improv':>8} {'%Impr':>7}") - print("-" * 80) - - sizes = [10, 50, 100, 200, 500, 1000] - for size in sizes: - insts = _gen_instructions(size, dep_chains=3) - dag_stats = bench_build_dag(insts, repeats=args.repeats) - sched_stats = bench_schedule(insts, repeats=args.repeats) - pct = (sched_stats["improvement"] / max(sched_stats["orig_cycles"], 1)) * 100 - print(f"{size:>8} {dag_stats['mean_s'] * 1000:>10.3f} " - f"{sched_stats['mean_s'] * 1000:>10.3f} " - f"{sched_stats['orig_cycles']:>8} {sched_stats['sched_cycles']:>8} " - f"{sched_stats['improvement']:>8} {pct:>6.1f}%") - - # Varying dependency depth - print(f"\nVarying Dependency Depth (200 instructions):") - print("-" * 70) - print(f"{'Chains':>8} {'Sched(ms)':>10} {'OrigCyc':>8} " - f"{'SchedCyc':>8} {'Improv':>8} {'%Impr':>7}") - print("-" * 70) - - for chains in [1, 2, 5, 10, 20]: - insts = _gen_instructions(200, dep_chains=chains) - sched_stats = bench_schedule(insts, repeats=args.repeats) - pct = (sched_stats["improvement"] / max(sched_stats["orig_cycles"], 1)) * 100 - print(f"{chains:>8} {sched_stats['mean_s'] * 1000:>10.3f} " - f"{sched_stats['orig_cycles']:>8} {sched_stats['sched_cycles']:>8} " - f"{sched_stats['improvement']:>8} {pct:>6.1f}%") - - # Large benchmark - print(f"\nLarge Block Stress Test (5000 instructions):") - print("-" * 60) - insts = _gen_instructions(5000, dep_chains=10, seed=99) - t0 = time.perf_counter() - scheduler = InstructionScheduler() - dag = scheduler.build_dag(insts) - scheduled = scheduler.schedule(dag) - elapsed = time.perf_counter() - t0 - print(f" DAG nodes: {len(dag)}") - print(f" Scheduled: {len(scheduled)}") - print(f" Time: {elapsed * 1000:.3f} ms") - print(scheduler.report(insts, scheduled)) + if args.repeats < 1 or args.max_region_size < 1: + parser.error("repeats and max-region-size must be positive") + records = [] + print( + "Synthetic inputs; local static model estimates, not measured hardware cycles" + ) + print( + f"{'Size':>6} {'Time(ms)':>10} {'Before':>8} {'After':>8} {'Saved':>8} {'Modeled':>8} {'Skipped':>8}" + ) + for size in [10, 50, 100, 200, 500, 1000, 5000]: + row = bench_schedule( + _gen_instructions(size), args.repeats, args.max_region_size + ) + row["seed"] = 42 + records.append(row) + before = str(row["orig_cycles"]) if row["modeled"] else "N/A" + after = str(row["sched_cycles"]) if row["modeled"] else "N/A" + saved = str(row["improvement"]) if row["modeled"] else "N/A" + print( + f"{size:6} {row['mean_s'] * 1000:10.3f} {before:>8} " + f"{after:>8} {saved:>8} {row['modeled']:8} {row['skipped']:8}" + ) + if args.json: + args.json.parent.mkdir(parents=True, exist_ok=True) + args.json.write_text(json.dumps(records, indent=2) + "\n", encoding="utf-8") + if args.markdown: + args.markdown.parent.mkdir(parents=True, exist_ok=True) + args.markdown.write_text(_markdown(records), encoding="utf-8") if __name__ == "__main__": diff --git a/benchmarks/cases/inst_scheduler_feature.asm b/benchmarks/cases/inst_scheduler_feature.asm new file mode 100644 index 0000000..6d65895 --- /dev/null +++ b/benchmarks/cases/inst_scheduler_feature.asm @@ -0,0 +1,10 @@ +# Deterministic RV32 scheduling case; initial state is supplied by the runner. +# a0=1024, t2=7, t3=0, memory[a0]=9, memory[a0+4]=0. +# The independent addi can fill the load-use stall: model cycles 5 -> 4. +# Expected output: t0=9, t1=16, t3=1, memory[a0+4]=16. +.text +scheduler_feature: + lw t0, 0(a0) + add t1, t0, t2 + addi t3, t3, 1 + sw t1, 4(a0) diff --git a/benchmarks/run_inst_scheduler_case.py b/benchmarks/run_inst_scheduler_case.py new file mode 100644 index 0000000..18a65a0 --- /dev/null +++ b/benchmarks/run_inst_scheduler_case.py @@ -0,0 +1,374 @@ +"""Report same-input scheduling A/B results through CompilerDriver. + +The default feature case must change order and pass real TinyFive execution. +Use --static-only for compiler-generated assembly: zero-hit inputs are valid, +and execution is explicitly not claimed. Model cycles are never CPU timings. +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import statistics +import time +from pathlib import Path +from typing import Any + +from scratchv.backend._asm_parser import parse_asm +from scratchv.backend.inst_scheduler import ( + ScheduleConfig, + parse_instructions, + schedule_assembly, +) +from scratchv.backend.riscv_encoder import assemble_to_binary +from scratchv.compiler import CompilerConfig, CompilerDriver +from scratchv.simulator.tinyfive import ProfiledMachine + +DEFAULT_CASE = Path(__file__).parent / "cases" / "inst_scheduler_feature.asm" +DATA_ADDRESS = 1024 +DATA_WORDS = [9] + [0] * 15 +INITIAL_REGISTERS = {10: DATA_ADDRESS, 7: 7} # a0, t2; other registers are zero. + + +def _instruction_count(source: str) -> int: + # Same executable-section scope as scheduling.input_instructions, including + # unsupported opcodes and excluding directives, data and display labels. + return len(parse_instructions(source)) + + +def _execute(source: str) -> dict[str, Any]: + """Execute the bounded integer feature-case contract without a fallback. + + Only straight-line code can use the encoded word count as an execution + limit. Keep a0 fixed and confine word accesses to the observed data area. + Arbitrary real programs belong in the static report, not this harness. + """ + for line in parse_asm(source): + if line.is_directive and line.opcode not in {"text", "globl", "global"}: + raise ValueError( + "feature execution only accepts text and global directives" + ) + instructions = parse_instructions(source) + if not instructions: + raise ValueError("feature execution requires nonempty straight-line RV32 code") + for inst in instructions: + effects = inst.effects + if ( + effects.barrier_reason + or effects.control != "none" + or effects.fp_flags + or any(not reg.startswith("x") for reg in inst.uses | inst.defines) + or "x10" in inst.defines + ): + raise ValueError( + "feature execution requires straight-line integer code with fixed a0" + ) + if effects.memory != "none": + address = effects.address + if ( + inst.opcode not in {"lw", "sw"} + or address is None + or address.base != "x10" + or not 0 <= address.offset < len(DATA_WORDS) * 4 + or address.offset % 4 + ): + raise ValueError( + "feature memory accesses must be aligned words within the a0 data area" + ) + + binary = assemble_to_binary(source) + if not binary or len(binary) % 4 or len(binary) > DATA_ADDRESS: + raise ValueError("invalid feature binary length or overlapping code and data") + words = [ + int.from_bytes(binary[i : i + 4], "little") for i in range(0, len(binary), 4) + ] + machine = ProfiledMachine(mem_size=4096) + if not machine.available: + raise RuntimeError("real TinyFive is unavailable; fallback is forbidden") + machine.load_binary(words, origin=0) + for index, value in enumerate(DATA_WORDS): + machine.write_mem_i32(DATA_ADDRESS + index * 4, value) + for index, value in INITIAL_REGISTERS.items(): + machine.set_reg(index, value) + machine.run(instructions=len(words), start=0, strict=True) + if ( + machine.last_error + or machine.instr_count != len(words) + or machine.pc != len(binary) + ): + raise RuntimeError( + machine.last_error or "TinyFive did not execute the complete feature case" + ) + return { + "encoded_instructions": len(words), + "code_size_bytes": len(binary), + "executed_instructions": machine.instr_count, + "all_registers": [machine.get_reg(i) for i in range(32)], + "memory_words": [ + machine.read_mem_i32(DATA_ADDRESS + i * 4) for i in range(len(DATA_WORDS)) + ], + "final_pc": machine.pc, + } + + +def run_case( + case_path: Path, *, static_only: bool = False, repeats: int = 3 +) -> dict[str, Any]: + """Measure only the scheduling toggle on identical input assembly.""" + if repeats < 1: + raise ValueError("repeats must be positive") + source = case_path.read_bytes().decode("utf-8") + baseline = CompilerDriver(CompilerConfig(schedule=False)) + before = baseline._run_asm_passes(source, []) + if before != source: + raise AssertionError("disabled scheduling changed the baseline assembly") + driver = CompilerDriver(CompilerConfig(schedule=True, schedule_strict=True)) + times = [] + for _ in range(repeats): + stats: dict[str, Any] = {} + warnings: list[str] = [] + started = time.perf_counter() + after = driver._run_asm_passes(source, warnings, stats) + times.append(time.perf_counter() - started) + if "schedule" not in stats: + raise AssertionError("CompilerDriver did not report scheduling statistics") + direct = schedule_assembly(source, ScheduleConfig(strict=True)) + if after != direct.asm_text: + raise AssertionError( + "CompilerDriver output differs from the public scheduling API" + ) + scheduling = stats["schedule"] + source_before, source_after = _instruction_count(before), _instruction_count(after) + if source_before != source_after: + raise AssertionError("scheduling changed the number of source instructions") + if scheduling["final_cycles"] > scheduling["original_cycles"]: + raise AssertionError("scheduling regressed the local cycle model") + changed = before != after + simulation: dict[str, Any] = { + "status": "not_run", + "backend": None, + "output_equal": None, + "reason": "Static assembly A/B only; no whole-program execution or hardware timing.", + } + errors = [] + if not static_only: + before_run, after_run = _execute(before), _execute(after) + registers_equal = before_run["all_registers"] == after_run["all_registers"] + memory_equal = before_run["memory_words"] == after_run["memory_words"] + simulation = { + "status": "passed" if registers_equal and memory_equal else "failed", + "backend": "tinyfive", + "fallback": False, + "initial_registers": {f"x{k}": v for k, v in INITIAL_REGISTERS.items()}, + "data_address": DATA_ADDRESS, + "initial_memory_words": DATA_WORDS, + "before": before_run, + "after": after_run, + "registers_equal": registers_equal, + "memory_equal": memory_equal, + "output_equal": registers_equal and memory_equal, + } + if ( + not changed + or scheduling["applied_regions"] == 0 + or scheduling["saved_cycles"] <= 0 + ): + errors.append( + "feature case did not exercise a beneficial scheduling change" + ) + if not simulation["output_equal"]: + errors.append("execution changed register or memory outputs") + return { + "schema_version": 1, + "benchmark_type": "assembly-ab" if static_only else "feature-case", + "case": str(case_path), + "input_sha256": hashlib.sha256(source.encode("utf-8")).hexdigest(), + "status": "failed" if errors else "passed", + "comparison_status": ( + "not_modeled" + if not scheduling["modeled_instructions"] + else "changed" + if changed + else "no_improvement" + ), + "errors": errors, + "feature": { + "name": "RV32 local instruction scheduling", + "baseline_schedule": baseline.config.schedule, + "compiler_config_schedule": driver.config.schedule, + "schedule_strict": driver.config.schedule_strict, + "compiler_path": "CompilerDriver._run_asm_passes", + "pipeline_matches_public_pass": True, + "used": changed and scheduling["applied_regions"] > 0, + "warnings": warnings, + }, + "source_instructions": {"before": source_before, "after": source_after}, + "scheduling": scheduling, + "timing": { + "repeats": repeats, + "mean_ms": statistics.mean(times) * 1000, + "stdev_ms": statistics.stdev(times) * 1000 if repeats > 1 else 0, + "scope": "Host elapsed time of CompilerDriver assembly post-pass; not target runtime", + }, + "simulation": simulation, + "assembly": {"before": before, "after": after}, + } + + +def _markdown(report: dict[str, Any]) -> str: + lines = [ + "# 课题 18:指令调度 A/B 报告", + "", + f"- 报告状态:{'PASS' if report['status'] == 'passed' else 'FAIL'}", + f"- 类型:`{report['benchmark_type']}`", + f"- 输入:`{report['case']}`", + ] + if "scheduling" not in report: + return "\n".join(lines + [f"- 错误:{report['error']}", ""]) + s, sim = report["scheduling"], report["simulation"] + source_count = report["source_instructions"]["before"] + coverage = s["modeled_instructions"] / source_count if source_count else 0 + modeled = s["modeled_instructions"] > 0 + lines.extend( + [ + f"- 输入 SHA-256:`{report['input_sha256']}`", + "- 编译器开关:`schedule=False` → `schedule=True, schedule_strict=True`", + f"- 编译器输出与独立调度 API 一致:`{report['feature']['pipeline_matches_public_pass']}`", + f"- 实际换序:`{report['feature']['used']}`", + f"- 比较结论:`{report['comparison_status']}`(changed=已换序;no_improvement=无收益;not_modeled=未建模)", + f"- 模型:`{s['model']}`;覆盖 {s['modeled_instructions']}/{source_count} 条源指令({coverage:.1%})", + f"- 应用区域:{s['applied_regions']};跳过/恢复区域:{s['skipped_regions']};移动指令:{s['moved_instructions']}", + f"- 调度耗时:{report['timing']['mean_ms']:.3f} ± {report['timing']['stdev_ms']:.3f} ms({report['timing']['repeats']} 次)", + "", + "## 指标", + "", + "| 指标 | 调度前 | 调度后 | 减少 |", + "|---|---:|---:|---:|", + f"| 源汇编指令数 | {report['source_instructions']['before']} | {report['source_instructions']['after']} | {report['source_instructions']['before'] - report['source_instructions']['after']} |", + f"| 局部模型周期(估算) | {s['original_cycles'] if modeled else 'N/A'} | {s['final_cycles'] if modeled else 'N/A'} | {s['saved_cycles'] if modeled else 'N/A'} |", + f"| 局部模型停顿(估算) | {s['original_stalls'] if modeled else 'N/A'} | {s['final_stalls'] if modeled else 'N/A'} | {s['original_stalls'] - s['final_stalls'] if modeled else 'N/A'} |", + ] + ) + if sim["status"] != "not_run": + a, b = sim["before"], sim["after"] + for label, key in [ + ("编码机器指令数", "encoded_instructions"), + ("代码大小(字节)", "code_size_bytes"), + ("TinyFive 实际执行指令数", "executed_instructions"), + ]: + lines.append(f"| {label} | {a[key]} | {b[key]} | {a[key] - b[key]} |") + lines.extend( + [ + "", + "## 执行验证", + "", + "- 后端:`tinyfive`;`fallback=false`。", + f"- 32 个整数寄存器一致:`{sim['registers_equal']}`。", + f"- 数据区 16 个字一致:`{sim['memory_equal']}`;输出一致:`{sim['output_equal']}`。", + "- 初始条件:其余寄存器为零,`a0=1024`、`t2=7`,数据区首字为 9、其余为零。", + "", + "| 观察值 | 调度前 | 调度后 |", + "|---|---:|---:|", + ] + ) + for index in (5, 6, 7, 28): + lines.append( + f"| x{index} | {a['all_registers'][index]} | {b['all_registers'][index]} |" + ) + lines.append( + f"| memory[1028] | {a['memory_words'][1]} | {b['memory_words'][1]} |" + ) + else: + lines.extend( + ["", "- 执行验证:未运行;机器码大小、动态指令数及硬件耗时均未测量。"] + ) + if report["errors"]: + lines.extend( + ["", "## 失败原因", "", *[f"- {error}" for error in report["errors"]]] + ) + lines.extend( + [ + "", + "> 周期是各局部区域在输入就绪假设下的静态估算,未覆盖部分不按零周期解释。", + "> 调度耗时是运行优化器的主机时间。TinyFive 仅验证执行结果和动态指令数,不能证明硬件加速。", + "", + "
", + "区域状态与诊断", + "", + "| 起止行 | 指令数 | 状态 | 模型周期(前 → 后) |", + "|---|---:|---|---|", + ] + ) + for row in s["regions"]: + before = row["original_cycles"] if row["original_cycles"] is not None else "N/A" + after = row["final_cycles"] if row["final_cycles"] is not None else "N/A" + lines.append( + f"| {row['start_line']}–{row['end_line']} | {row['instructions']} | {row['status']} | {before} → {after} |" + ) + lines.append("") + for diagnostic in s["diagnostics"]: + lines.append(f"- 第 {diagnostic['line']} 行:{diagnostic['reason']}") + lines.extend(["", "
"]) + for key, label in [("before", "调度前汇编"), ("after", "调度后汇编")]: + lines.extend( + [ + "", + "
", + f"{label}", + "", + "```asm", + report["assembly"][key].rstrip(), + "```", + "", + "
", + ] + ) + return "\n".join(lines) + "\n" + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("case", nargs="?", type=Path, default=DEFAULT_CASE) + parser.add_argument( + "--static-only", + action="store_true", + help="Report supplied assembly without executing it; zero-hit cases are valid", + ) + parser.add_argument("--repeats", type=int, default=3) + parser.add_argument( + "--json", + type=Path, + default=Path("benchmark_reports/inst_scheduler_report.json"), + ) + parser.add_argument( + "--markdown", + type=Path, + default=Path("benchmark_reports/inst_scheduler_report.md"), + ) + args = parser.parse_args(argv) + if args.repeats < 1: + parser.error("repeats must be positive") + try: + report = run_case(args.case, static_only=args.static_only, repeats=args.repeats) + except Exception as exc: # noqa: BLE001 - Persist a failed CI report for unexpected errors too. + report = { + "schema_version": 1, + "benchmark_type": "assembly-ab" if args.static_only else "feature-case", + "case": str(args.case), + "status": "failed", + "error": f"{type(exc).__name__}: {exc}", + } + markdown = _markdown(report) + for path, content in [ + (args.json, json.dumps(report, indent=2, ensure_ascii=False) + "\n"), + (args.markdown, markdown), + ]: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content, encoding="utf-8") + print(markdown) + return 0 if report["status"] == "passed" else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git "a/docs/topic-18/18-\346\214\207\344\273\244\350\260\203\345\272\246\345\231\250Benchmark.md" "b/docs/topic-18/18-\346\214\207\344\273\244\350\260\203\345\272\246\345\231\250Benchmark.md" new file mode 100644 index 0000000..5d3a22f --- /dev/null +++ "b/docs/topic-18/18-\346\214\207\344\273\244\350\260\203\345\272\246\345\231\250Benchmark.md" @@ -0,0 +1,109 @@ +# 课题 18:Benchmark 与 CI 报告 + +状态:2026-09-16 review 迭代后的验证入口。将固定功能用例、实际编译输出、合成规模与独立 CPU 模型审计分别展示。前后对比结果见 [Review 迭代报告](18-指令调度器Review迭代报告.md)。 + +## 环境准备 + +从仓库根目录运行,推荐 Python 3.12(CI 基线)。下面的 `python` 指选定的解释器;使用已有虚拟环境前先检查版本,不能只凭 `.venv` 目录名认定兼容。 + +```bash +python --version +python -m pip install -e '.[riscv]' 'pytest>=7,<10' +``` + +本次本地测试使用 Python 3.14.7;review 中的 Python 3.8 是评审方当时的环境,不是所有 checkout 的固定版本。项目包元数据仍声明较低的 Python 下限,但本报告不据此承诺全部模块兼容 3.8。 + +真实 RISC-V 执行测试还需要 `clang`、`ld.lld` 和 `qemu-riscv32`;独立性能审计需要带 RISC-V 支持的 `llvm-mca`。Ubuntu CI 安装 `clang-18 lld-18 llvm-18 qemu-user`。 + +## 固定功能用例:编译器集成与真实执行 + +完成上述环境准备后运行: + +```bash +python -m benchmarks.run_inst_scheduler_case +``` + +默认读取 `benchmarks/cases/inst_scheduler_feature.asm`,输出: + +- `benchmark_reports/inst_scheduler_report.json` +- `benchmark_reports/inst_scheduler_report.md` + +先将同一份汇编分别交给 `CompilerDriver._run_asm_passes` 的 `schedule=False` 和 `schedule=True, schedule_strict=True` 配置,确认启用后的输出与 `schedule_assembly` 一致,再编码前后汇编并分别用真实 TinyFive 执行。其它汇编优化开关均使用关闭的默认值。 + +用例依次执行 load、依赖 load 的 add、独立 addi、store。调度后将 addi 放到 load 与 add 之间。初始状态固定为 `a0=1024`、`t2=7`、其它寄存器为零;数据区为 16 个字,首字为 9,其余为零。 + +| 指标 | 调度前 | 调度后 | +| --- | ---: | ---: | +| 源汇编指令数 | 4 | 4 | +| 局部模型周期(估算) | 5 | 4 | +| 局部模型停顿(估算) | 1 | 0 | +| 编码机器指令数 | 4 | 4 | +| 代码大小(字节) | 16 | 16 | +| TinyFive 实际执行指令数 | 4 | 4 | + +执行比较覆盖全部 32 个整数寄存器和数据区的 16 个字。默认用例的预期结果为 `t0=9`、`t1=16`、`t3=1`、`memory[1028]=16`,测试同时检查这些值。运行器要求用例确实发生有收益的换序,且前后执行结果一致;TinyFive 缺失、执行失败或结果不一致都会生成 FAIL 报告并返回非零退出码,不允许静态分析替代执行。 + +这个执行入口只接受有界的直线整数代码,固定 `a0`,访存限定在上述数据区内的对齐 `lw/sw`。分支、调用、浮点或任意地址的程序不能使用“执行指令数等于编码指令数”的终止方法,因此会被明确拒绝。 + +## 实际编译输出:同一份汇编的静态 A/B + +先生成待比较的汇编。以下命令依赖上面的 editable 安装;已有 CNN 模型时直接使用,缺失时才生成最小模型: + +```bash +mkdir -p benchmark_reports models/graph +test -f models/graph/cnn.onnx || python scripts/gen_minimal_cnn.py +PYTHONPATH=. python scratchv/standalone/onnx_to_riscv_standalone.py \ + models/graph/cnn.onnx -o /tmp/topic18-cnn.bin \ + --asm benchmark_reports/cnn_scratchv.s --estimate --report --const-merge +``` + +```bash +python -m benchmarks.run_inst_scheduler_case \ + benchmark_reports/cnn_scratchv.s --static-only \ + --json benchmark_reports/inst_scheduler_cnn.json \ + --markdown benchmark_reports/inst_scheduler_cnn.md +``` + +该命令同样通过编译器的调度开关处理相同输入,记录 SHA-256、源指令数、调度模型、建模覆盖率、移动指令数、区域状态、诊断以及优化器耗时。所有无收益、遇到边界或超过大小上限的区域都保留在报告中。 + +`--static-only` 的 JSON 类型为 `assembly-ab`,执行状态为 `not_run`,`output_equal` 为 `null`。它不声称 CNN 已经端到端执行,也不报告未经编码或执行测量的机器码大小和动态指令数。零收益是有效测量结果,不导致失败;没有指令被建模时,Markdown 中的周期和停顿显示 `N/A`。 + +`status=passed` 表示报告运行和相应校验通过;`comparison_status` 单独区分 `changed`(已换序)、`no_improvement`(无收益)和 `not_modeled`(未建模)。当前 standalone CNN 列表包含数字分支偏移,例如 `bne t4, zero, -48`,会触发调度器的整份输入保留规则。本地报告因此为 `not_modeled`,不能解释为 CNN 加速或已完成 CNN 执行验证。 + +源指令数与 `scheduling.input_instructions` 现在使用同一个计数入口:统计执行段指令行,不计 `_op_/layer1/Conv:` 等显示标签、指示行和数据段。保留未知 opcode 与伪指令,一条伪指令行计为一条源指令。这不是编码后的机器指令数。 + +## 合成规模测试 + +```bash +python -m benchmarks.bench_inst_scheduler --repeats 3 \ + --json benchmark_reports/inst_scheduler_synthetic.json \ + --markdown benchmark_reports/inst_scheduler_synthetic.md +``` + +使用固定种子 42,覆盖 10、50、100、200、500、1000、5000 条指令。默认区域上限为 1024,因此 5000 条的整块用例跳过并显示 `N/A`;可用 `--max-region-size` 调整上限。 + +JSON 继续使用数组格式并保留原有统计字段,新增 `benchmark_type=synthetic`、输入哈希、种子、重复次数、区域上限、区域数量与 `execution_verified=false`。原有周期字段仍为模型对已覆盖区域的求和;必须结合 `modeled` 和 `skipped` 解读,不能将未建模的零计数当作完整程序耗时。 + +本轮新增 `sensitivity_rejected_regions`:主模型预测收益、另一组延迟预测无收益时保留原序,单独计数。不要继续使用旧模型的 240→187 等数字描述本版。 + +## 独立性能与真实覆盖审计 + +```bash +python -m benchmarks.audit_inst_scheduler --llvm-mca llvm-mca-18 +``` + +可将工具参数替换为本机 `llvm-mca` 的路径。本次本地使用 LLVM 22.1.8,CI 使用 LLVM 18;工具版本写入报告,跨版本数字不能直接混算。 + +该入口复用 review 的 72 个合成样例(规模 10/50/100/200/500/1000,种子 42/1/2,依赖链 1/2/3/8),在 `rocket-rv32` 和 `sifive-e76` 上分别以 1 次迭代比较相同输入调度前后的输出。此外,用 greedy、linear 编译 `benchmarks/cases` 的全部编号 DSL 和 `models/graph/cnn.onnx`,记录所有文件的覆盖率,并用 MCA 检查已应用区域的指令体;固定终止指令不计入这种局部体估算。 + +默认生成 `benchmark_reports/inst_scheduler_audit.json` 和 `.md`。工具缺失、编译失败、MCA 不支持输入均失败;合成数据要求每个 CPU 总节省 > 0 且胜例数 ≥ 负例数,真实已应用区域要求总节省 ≥ 0 且胜例数 ≥ 负例数。完整 JSON 保留负例、零收益、输入文件及汇编哈希、逐区域估算和诊断。总体通过不代表每个样例都变快。 + +CI 的 `scheduler-review` job 单独安装工具链,设置 `SCRATCHV_REQUIRE_RISCV_EXECUTION=1`,使执行测试缺工具时直接失败;审计报告写入 Job Summary 并上传为 `scheduler-review-report` artifact。CI 缺少未跟踪的 CNN 模型时生成最小 CNN,它与本地已有模型可能不同,应结合哈希比较。 + +## 统计口径与 CI + +- 模型周期只表示局部调度区域的静态估算,假设区域入口操作数就绪,不包含缓存、分支预测和程序路径执行次数。 +- 优化器耗时是主机执行调度 pass 的时间,以重复运行的均值和标准差报告,不是目标程序运行时间。 +- TinyFive 提供固定用例的执行结果与动态指令数,不能证明硬件周期下降。调度只改变顺序,示例中的指令数和代码大小保持不变。 +- CI 的固定用例和合成测试分别生成报告;CNN 步骤生成 `cnn_scratchv.s` 后,再运行静态 A/B。该输入沿用现有 CNN 编译步骤的 `--const-merge` 输出,比较两侧使用完全相同的汇编,只切换调度。 +- 三组 JSON/Markdown 都由现有 `benchmark-reports` artifact 收集,Markdown 同时写入 GitHub Actions Job Summary。报告文件属于生成产物,保留在已被忽略的 `benchmark_reports/` 中。 diff --git "a/docs/topic-18/18-\346\214\207\344\273\244\350\260\203\345\272\246\345\231\250Review\350\277\255\344\273\243\346\212\245\345\221\212.md" "b/docs/topic-18/18-\346\214\207\344\273\244\350\260\203\345\272\246\345\231\250Review\350\277\255\344\273\243\346\212\245\345\221\212.md" new file mode 100644 index 0000000..689c221 --- /dev/null +++ "b/docs/topic-18/18-\346\214\207\344\273\244\350\260\203\345\272\246\345\231\250Review\350\277\255\344\273\243\346\212\245\345\221\212.md" @@ -0,0 +1,116 @@ +# Topic 18:Review 迭代报告 + +日期:2026-09-16。基线:`035d180`。评审输入:[topic18-review.md](topic18-review.md) 的第二轮 F1–F16;T1–T7 属于另一个对照分支,不在本轮修复范围内。候选为包含本报告的 review 迭代提交。 + +本轮修复四项 P1 对应的代码、文档及仓库卫生问题,并补充覆盖率、段状态处理和独立性能审计。**实现仍是默认关闭的文本 V1;独立模型总体验收通过,但仍有负优化样例,不能宣传为普遍硬件加速。** + +## 1. 逐项处理结果 + +| 编号 | 本轮处理 | 状态 / 保留事项 | +|---|---|---| +| F1 | 将 `:memory:.ses` 从 Git 索引移除,添加根目录忽略规则 | 已处理。运行期间该本地会话文件会重建,因此保留本地文件,仅取消跟踪;没有移除 `docs/topic-18` 的跟踪 | +| F2 | 新增文档状态页,重写当前代码说明,历史计划和设计标记版本,移除失效链接的验收依赖 | 已处理;结构化 V2 仍为 backlog | +| F3 | 在机器指令对象中将整数寄存器源位置上的 0 规范为 `zero`;补充覆盖率、未建模 opcode 计数和 warning | 核心改进完成;`max` 及非零字面量占据寄存器位置的形式仍固定,不把 1 等常量错误解释为 x0 | +| F4 | 多行报告移入 `stats["schedule"]["report"]`,CLI 独立打印 | 已处理;warnings 只承载诊断 | +| F5 | 不安全布局诊断指向实际命中行,整文件跳过升为 warning | 定位已修复;宏、条件汇编、数字跳转等仍整份保护,尚未缩小影响范围 | +| F6 | 明确记录独立注释、空行、同行标签作为边界 | 采用文档化保守行为;未猜测注释归属 | +| F7 | 在 CHANGELOG 和代码说明中列出不可变语义对象、跨区拒绝及有损转换拒绝的迁移说明 | 已处理;推荐完整汇编使用 `schedule_assembly` | +| F8 | 保留 1024 条默认上限,并明确配置入口和跳过口径 | 分窗调度延后;5000 条整区仍显示 N/A | +| F9 | 允许空 legacy 延迟字典与显式模型共用;修复引号误报;说明两份 report API 的兼容用途 | 保留 `_run_asm_passes` 后端校验,保护直接调用该入口的情形 | +| F10 | 新增 `scheduler-review` CI job,安装 clang/lld/QEMU/llvm-mca,工具缺失时执行测试必须失败;增加两个 CPU 的 A/B 验收 | 本地执行验证及审计通过;GitHub 新 job 尚待本次修改推送后执行 | +| F11 | 乘法 4 拍、整数除法 33 拍;除法保守阻塞发射并保持相对顺序;补 WAW 完成时间;主模型及另一组延迟必须同时改善 | 本轮总体门槛通过,review 最小负例保留原序;完整 CPU 模型选择与硬件校准仍未实现 | +| F12 | `LsInstruction.target` 独立保存目标,标签发射为 `label:`,分支目标发射为操作数;分支/store 首操作数按读取处理 | 编码回归通过;新增 QEMU 实际执行倒向循环的验证 | +| F13 | 新增 23 个编号 DSL + CNN、greedy/linear 两条路径的全量覆盖审计,保留零收益文件和区域大小 | 测量和告警完成;未跨基本块合并,仍只有 3/24 个真实输入变化 | +| F14 | 引号感知的布局检查,恢复 push/pop/previous 段状态,异常恢复诊断,明确数据段及非执行 flags 保护 | 已修复所列误报与段状态缺陷;真正危险布局仍按 F5 保守保护 | +| F15 | 写明 Python 版本检查、editable 安装与工具链前提;standalone 命令显式设置 `PYTHONPATH=.` | 已处理;不把评审方的 Python 3.8 当成每个环境的现状 | +| F16 | Benchmark 与调度器共用源指令计数范围,显示标签不再计为未知指令 | 已处理;本地 CNN 两侧均为 876 条,不再出现 876 / 889 两种口径 | + +## 2. 模型修正为什么这样做 + +仅把除法延迟从 16 改成 33,或者只禁止除法跨过其他指令,并不能消除模型偏差。本轮采用三层限制: + +1. 除法保持相对位置,并在教学模型中保守阻塞后续发射。 +2. 对物理寄存器 WAW 计入完成时间,避免把较早的长延迟写入当作已经结束。 +3. 用乘法 3、除法/余数 66 的第二组参数检查候选。只有两组估算都严格改善才接受;否则记录 `model_sensitive` 并保留原序。 + +第二组参数来自 LLVM SiFive7 的整数延迟范围,但不是完整 E76 模拟器。该检查仍采用单发射及简化资源规则;真实 CPU 的发射宽度、旁路、缓存和动态除法时长未被完整表达。 + +默认三条依赖链、种子 42 的合成规模测试(10–1000 条)在本轮全部保留原序,模型收益为 0;5000 条仍因超限未建模。这里保留零收益,不挑选替代样例制造改善。独立 72 例审计覆盖更多依赖链形态,另行统计总体效果。 + +## 3. 独立 llvm-mca 前后对照 + +以下基线和候选均在同一台机器、**LLVM 22.1.8** 下重跑。评审原文使用 LLVM 18.1.8,部分统计不同;不能把不同版本的数字直接混在同一列比较。两边使用相同的 72 个输入及其 SHA-256:规模 10/50/100/200/500/1000 × 种子 42/1/2 × 依赖链 1/2/3/8,每个输入 MCA 迭代数为 1。 + +| CPU | 实现 | 原序总周期 | 调度后总周期 | 总节省 | 胜 / 平 / 负 | +|---|---|---:|---:|---:|---| +| rocket-rv32 | 基线 | 103055 | 102285 | 770 | 25 / 9 / 38 | +| rocket-rv32 | 本轮 | 103055 | 102105 | 950 | 22 / 41 / 9 | +| sifive-e76 | 基线 | 175522 | 175441 | 81 | 28 / 5 / 39 | +| sifive-e76 | 本轮 | 175522 | 174608 | 914 | 24 / 37 / 11 | + +验收规则为:每个 CPU 总节省 > 0,且胜例数 ≥ 负例数。本轮两者均通过,基线两者均未通过。**仍有 Rocket 9 个、E76 11 个负例**,完整结果保存在 [review-iteration-results.json](review-iteration-results.json),没有从统计中剔除。 + +review 最小负例(100 条、seed=42、chains=3,输入 SHA-256 `31a9c756dd15e42660ba930fc5020ee119eb2bcb3782cb6591abe99e8098a975`): + +| 实现 | 本地模型 | Rocket MCA | E76 MCA | +|---|---|---|---| +| 基线 | 240→187 | 449→462 | 769→788 | +| 本轮 | 457→457,保留原序 | 449→449 | 769→769 | + +两版本地模型的绝对周期没有可比性;可比较的是在同一个独立 MCA 中各自调度前后的差异。上述结果均为静态 CPU 模型估算,不是硬件实测。 + +## 4. 真实编译输入的覆盖率 + +审计逐一编译 23 个编号 DSL 与本地 `models/graph/cnn.onnx`,在 greedy 和 linear 两种分配器下分别生成汇编。对每一份汇编只切换调度开关。输入模型、原始汇编及调度后汇编哈希见 JSON。 + +| 分配器 | 指标 | 基线 | 本轮 | +|---|---|---:|---:| +| greedy | 建模 / 输入指令 | 180 / 211 | 187 / 211 | +| greedy | 覆盖率 | 85.3% | 88.6% | +| greedy | 发生变化的文件 | 3 / 24 | 3 / 24 | +| linear | 建模 / 输入指令 | 104 / 199 | 175 / 199 | +| linear | 覆盖率 | 52.3% | 87.9% | +| linear | 发生变化的文件 | 1 / 24 | 3 / 24 | + +本轮平均区域长度为 greedy 2.23、linear 2.08,最大分别为 10、5,仍然很小。两条路径各剩 24 条未建模指令,主要包括 `max`、带非零立即数的寄存器形式及多指令伪操作。没有通过放宽未知语义来提高覆盖率。 + +本轮两条路径合计 12 个已应用区域,在 MCA 中检查其指令体(固定终止指令除外):Rocket 87→75,胜/平/负=12/0/0;E76 70→63,胜/平/负=7/5/0。这是已应用区域的局部估算,不能相加解释为 CNN 或完整程序运行时间。 + +另行复跑 standalone CNN 静态 A/B:源指令数与调度器输入数统一为 876;数字分支位于第 22 行,诊断准确定位到该行,结果仍为 `not_modeled`、两份汇编一致、未运行完整 CNN。这个入口与上面的 CompilerDriver CNN 路径不同。 + +## 5. 正确性与工程验证 + +- 全仓:`901 passed`,无跳过;另有 11 个既有 pytest marker warning。 +- 原有 19 个 clang + QEMU 对拍实际执行;新增 linear 倒向循环 QEMU 用例通过。 +- 三个循环/控制流 DSL 在 linear、调度开/关两种配置下均可由仓库编码器生成非空机器码,目标标签存在,调度不再因丢失目标而整文件熔断。 +- 固定 TinyFive 用例:32 个整数寄存器与 16 个数据字一致;源指令/编码指令/动态执行指令均为 4→4,模型 5→4,停顿 1→0。 +- 新增回归覆盖引号内特殊字符、嵌套段栈、previous 切换、数据 flags、异常恢复、显示标签、诊断定位、覆盖缺口、敏感性拒绝、WAW 成本与零寄存器规范化。 +- 工作流 YAML 可解析;`git diff --check` 通过。新 GitHub Actions job 尚未在远端执行,不能将本地验证等同于远端 CI 已通过。 + +## 6. 复现与产物 + +从仓库根目录运行,使用已安装依赖的 Python 3.12+ 环境。完整环境和 CNN 生成命令见 [Benchmark](18-指令调度器Benchmark.md)。 + +```bash +SCRATCHV_REQUIRE_RISCV_EXECUTION=1 python -m pytest tests/ -q \ + --junit-xml=benchmark_reports/topic18_iteration_tests.xml +python -m benchmarks.run_inst_scheduler_case +python -m benchmarks.bench_inst_scheduler --repeats 3 \ + --json benchmark_reports/inst_scheduler_synthetic.json \ + --markdown benchmark_reports/inst_scheduler_synthetic.md +python -m benchmarks.audit_inst_scheduler --llvm-mca llvm-mca-18 +python -m benchmarks.run_inst_scheduler_case \ + benchmark_reports/cnn_scratchv.s --static-only \ + --json benchmark_reports/inst_scheduler_cnn.json \ + --markdown benchmark_reports/inst_scheduler_cnn.md +``` + +本地实际使用 `.venv/bin/python` 3.14.7,以及下载到 `/tmp/topic18-llvm/usr/bin/llvm-mca` 的 LLVM 22.1.8;没有安装或更换系统 LLVM 包。CI 配置为 Python 3.12 / LLVM 18,并在干净 runner 上生成最小 CNN;该模型可能与本地已有 CNN 不同,比较时必须看输入哈希。 + +基线复跑方法:将 `035d180` 中的 `scratchv`、`scratchv_dag`、`benchmarks` 用 `git archive` 解到临时目录,通过 `PYTHONPATH` 指向该目录,再运行候选中的 `benchmarks/audit_inst_scheduler.py`。这使同一审计脚本和同一 MCA 驱动旧实现;没有切换或修改工作区代码。 + +可随 PR 提交的记录是本文与 `review-iteration-results.json`。完整原始 JSON、Markdown、JUnit 和生成日志在被忽略的 `benchmark_reports/` 中;CI 会上传 `scheduler-review-report` artifact。 + +## 7. 后续工作边界 + +本轮没有实现完整目标 CPU 配置、消除所有负例、Fast/BURR、结构化 post-RA 调度、跨基本块调度、别名放宽或大区域分窗。建议后续优先针对 JSON 中保留的负例校准发射/旁路模型,并处理上游非零立即数放在寄存器位置的发射问题。在这些工作完成前,继续保持调度默认关闭。 diff --git "a/docs/topic-18/18-\346\214\207\344\273\244\350\260\203\345\272\246\345\231\250SPEC-Review.md" "b/docs/topic-18/18-\346\214\207\344\273\244\350\260\203\345\272\246\345\231\250SPEC-Review.md" new file mode 100644 index 0000000..9d6f33c --- /dev/null +++ "b/docs/topic-18/18-\346\214\207\344\273\244\350\260\203\345\272\246\345\231\250SPEC-Review.md" @@ -0,0 +1,354 @@ +# Topic 18 指令调度器SPEC 评审 + +> 状态:对早期 SPEC 和旧基线的历史评审。本文的“当前”均指当时评审环境,不代表 2026-09-16 的实现。被评审的 SPEC 及文中提到的 Joska 详细设计未随仓库提供,不作为本版验收依赖。已采纳的范围调整及后续状态见 [README](README.md);当前代码见 [代码说明](18-指令调度器代码说明.md)。 + +## 综述 + +本文对 `18-指令调度器SPEC.md`(下文称"SPEC")进行独立技术评审。SPEC 提出在寄存器分配后、汇编发射前,对结构化 `MachineInstr` 执行基本块内列表调度。评审基于当前 ScratchV 代码仓(`/root/Lab/ScratchV` `main`)的实际状态。 + +| 文档 | 路径 | 版本 | +|---|---|---| +| **SPEC** | `18-指令调度器SPEC.md` | Proposed v0.1 (2026-07-25) | +| **代码基线** | `/root/Lab/ScratchV` | `main` | + +--- + +## 课题意义:LLVM 指令调度与编译流程定位 + +### 为什么需要指令调度? + +指令调度是编译器后端中**与微架构最接近的优化 pass**。它的核心目标是:在不改变程序语义的前提下,通过重排指令顺序来减少流水线停顿(pipeline stall),从而降低 CPI(Cycles Per Instruction)。 + +在现代处理器中,一条指令从取指到写回需要多个周期(典型的 RISC-V 5 级流水线:IF→ID→EX→MEM→WB)。当一条指令的结果被下一条指令立即使用时,流水线必须停顿等待结果就绪——这就是**数据冒险(data hazard)**。指令调度通过把不相关的独立指令插入到这些"等待槽"中,在不增加指令数的前提下隐藏延迟。 + +以经典的 load-use 场景为例: + +```asm +; 调度前:4 周期,1 个 stall +lw t0, 0(a0) # @0 发射,结果 @2 就绪 +add t1, t0, t2 # @1 发射?不,t0 未就绪 → stall @1 + # @2 发射 add +lw t3, 4(a0) # @3 发射 + +; 调度后:3 周期,0 个 stall +lw t0, 0(a0) # @0 发射 +lw t3, 4(a0) # @1 发射(独立指令填充 load-use stall 槽) +add t1, t0, t2 # @2 发射(t0 已就绪) +``` + +调度不减少指令总数(仍然是 3 条),也不改变数据流——它只是让独立指令占据原本空闲的发射槽。 + +### LLVM 中的指令调度 + +LLVM 作为工业级编译器,包含了多个不同粒度和层次的调度 pass,覆盖从 SelectionDAG 到机器指令的完整流程: + +| LLVM Pass | 阶段 | 调度对象 | 目标 | +|---|---|---|---| +| `DAGCombiner` | SelectionDAG 构建中 | DAG 节点 | 简化 DAG、消除冗余节点 | +| `DAGScheduler` | SelectionDAG → MachineInstr | SelectionDAG 节点 | 确定指令选择顺序、暴露 ILP | +| `MachineScheduler` | MachineInstr 阶段(post-RA) | 机器指令(MachineInstr) | 减少流水线数据冒险 | +| `PostRAScheduler` | 寄存器分配后 | 物理寄存器指令 | 最后一轮调优 | +| `MachineBlockPlacement` | 机器指令阶段 | 基本块布局 | 改善分支预测局部性 | + +其中 **MachineScheduler** 是真正意义上的"指令调度"——它在寄存器分配之后、汇编发射之前工作,此时所有指令都已绑定到物理寄存器,调度器拥有完整的 def-use 信息和精确的延迟模型。LLVM 的 MachineScheduler 支持多种调度策略(VLIW、Latency、ILP),并通过 `ScheduleDAG` + `SUnit` 数据结构构建依赖图,使用基于优先级的列表调度或回溯调度生成最终顺序。 + +### 调度前后的编译器在做什么 + +理解调度在编译流程中的位置,有助于看清它负责什么、不负责什么: + +``` +指令调度前(上游 pass): + ┌─ 指令选择(Instruction Selection) + │ 将目标无关 IR 转换为目标相关的 MachineInstr 或汇编指令。 + │ 输出:指令序列(顺序接近 IR 遍历顺序,未优化)。 + │ + └─ 寄存器分配(Register Allocation) + 将虚拟寄存器映射到物理寄存器,插入 spill/reload 代码。 + 输出:含物理寄存器的指令序列。 + ↑ 调度发生在此之后 + ↓ + 调度器(Instruction Scheduling) + 分析依赖 → 构建 DAG → 计算优先级 → 列表调度。 + 输出:重排后的指令序列(指令集合不变,顺序改变)。 + ↑ 调度发生后 + ↓ +指令调度后(下游 pass): + ┌─ 汇编发射(Assembly Emission) + │ 将指令序列输出为汇编文本格式。 + │ + ├─ 窥孔优化(Peephole Optimization)[可选] + │ 修复调度可能暴露的局部次优模式。 + │ + ├─ 形式美化(Beautifier)[可选] + │ 调整缩进、对齐等不影响语义的格式。 + │ + └─ 周期估算 / 汇编(Cycle Estimation / Assembler) + 估算调度收益或生成最终二进制。 +``` + +**关键理解**: +- **调度不改变指令集合**——它不删除、不添加、不替换指令。它只是重新排序。 +- **调度的收益来源于流水线时序**——而不是指令数量。CPI 可以改善,指令数不变。 +- **调度不能修复寄存器分配引入的 spill**——如果 RA 产生了多余的 load/store,调度只能重排它们,不能消除它们。 +- **调度与架构无关**——通用的列表调度算法适用于任何单发射/多发射处理器,只需更换微架构模型参数。 + +### 为什么 ScratchV 需要指令调度? + +ScratchV 当前的指令顺序接近 IR 遍历顺序,没有经过任何流水线感知的重排。这意味着: + +1. **load-use 模式必然产生 stall**——`lw` 后紧跟使用该值的指令是最常见的可优化模式 +2. **多周期指令(mul、div)后的独立指令没有被利用**——这些指令周围通常有空闲周期 +3. **浮点运算的长延迟未被隐藏**——`fdiv.d` 延迟 16 周期,足够填充大量独立指令 +4. **没有收益量化机制**——即使调度发生了改变,也没有周期估算来证明其收益 + +ScratchV 的指令调度不追求 LLVM 级别的调度复杂度(多发射、乱序、软件流水),但即使在单发射顺序流水线上,保守的列表调度也足以在典型计算密集型 kernel(如 Conv、GEMM、矩阵乘)中获得 5-15% 的 CPI 改善——这正是 Topic 18 的立项依据。 + +--- + +## 1. 总体评价 + +SPEC 是一份高质量的技术设计文档,架构分析深入、正确性约束严谨、对当前实现的诊断准确。但它存在显著的**工程可行性 gap**——设计的理论正确性在 ScratchV 当前代码状态下落地成本过高,且其核心主张(调度位置前移、线性分配器重构)在当前阶段不具备可执行条件。 + +| 维度 | 评分 | 说明 | +|---|---|---| +| **架构正确性** | 9/10 | 设计在正确位置(post-RA/pre-emission)是最优架构 | +| **代码仓吻合度** | 4/10 | 设计与代码仓默认配置(linear allocator)、pass 编排、MachineInstr 能力存在根本性 mismatch | +| **工程可执行性** | 4/10 | 需要先修改 3 个子系统才能开始实现核心调度逻辑 | +| **可测试性** | 8/10 | 11 条不变量条条可测试,验证器设计清晰 | +| **文档完整性** | 8/10 | 结构完整,但缺少与其他组件交互的具体代码级描述 | + +--- + +## 2. 设计优点(值得肯定的部分) + +### 2.1 对当前代码缺陷的诊断极其精准 + +SPEC 16.2 节对当前实现的分析是全文最有价值的部分。经代码仓验证: + +| SPEC claim | 代码仓证据 | 严重性 | +|---|---|---| +| 生产调度发生在 AsmEmitter 之后 | `compiler.py:458-469` 确认 | 高 | +| 汇编 parser 根据 operand 位置猜测 def-use | `inst_scheduler.py:406-430`、`_asm_parser.py:245-278` | 高 | +| 没有机器基本块分区 | `inst_scheduler.py` 无任何 label/terminator 感知逻辑 | 高 | +| 构图仅实现 RAW 和 WAW | `inst_scheduler.py:186-207` 无 WAR、无 memory、无 control | 高 | +| cycle report 只累加 opcode latency | `inst_scheduler.py:319-324` `estimate_cycles` | 高 | +| 关键路径只检查 priority > 0 | `tests/test_inst_scheduler.py:157-158` | 中 | +| `machine_instrs_from_scheduled` 回退到 MV | `inst_scheduler.py:480-481` | 中 | + +这 8 条诊断全部精确,直接可作为重构需求清单。 + +### 2.2 11 条不变量定义系统化 + +SPEC 第 10 节定义的 11 条正确性不变量(置换、边界、依赖、控制、结构、寄存器、确定性、禁用、报告、应用、失败原子性)是格式良好的正确性规格,可翻译为自动化测试。与详细设计的验证策略相比,SPEC 的优点是: + +- 不变量与实现分离,可作为独立测试契约 +- 失败原子性(第 11 条)确保部分调度结果不会污染输出 +- 禁用不变量(第 8 条)保证 `--schedule=off` 时字节级不变 + +### 2.3 组件分解合理 + +SPEC 第 5 节将调度器分解为 7 个职责单一的组件,边界清晰。这比当前 `inst_scheduler.py` 的单类实现更可维护。 + +### 2.4 正确的调度位置分析 + +SPEC 4.1 节对 post-RA/pre-emission 位置的分析是正确的——这是工业编译器(LLVM、GCC)的标准做法。结构化 IR 调度优于文本正则解析,这是不容置疑的架构常识。 + +--- + +## 3. 设计与代码仓现状的 gap(核心问题) + +### 3.1 默认寄存器分配器与 SPEC 的前提矛盾(致命 gap) + +**前提**:SPEC 要求所有寄存器分配路径返回结构化 `MachineInstr` 列表(第 4.3 节)。 + +**现状**:`CompilerConfig` 默认 `reg_alloc="linear"`(`compiler.py:60`),而 linear-scan allocator 直接输出汇编文本: + +```python +# compiler.py:407-413 +if self.config.reg_alloc == "linear": + from scratchv.backend.regalloc_linear import ... + lsa = LinearScanAllocator() + return lsa.emit(ls_insts) # 返回 str,不是 list[MachineInstr] +``` + +**后果**: +- 在默认配置下,SPEC 方案无法启动 +- SPEC 4.3 节的"配置验证拒绝 `--reg-alloc linear --schedule`"方案意味着: + - 用户必须额外传递 `--reg-alloc greedy` 才能启用调度 + - 如果 `greedy` allocator 有 bug 或行为差异,用户会面临两难选择 +- 要使 SPEC 完全可工作,需要重写 linear-scan 分配器的输出路径(约 200-300 行代码量级) + +**严重性**:高。SPEC 应明确讨论此依赖的工程成本。 + +### 3.2 调度位置与 CompilerDriver 的集成 gap(架构级差异) + +**SPEC 主张**: +``` +IR → InstrSelect → RegAlloc → [Schedule] → AsmEmit +``` + +**当前代码**(`compiler.py:397-418`): +```python +def _generate_riscv_linear(self, program) -> str: + selector = InstructionSelector(program) + machine_instrs = selector.run() + # linear-scan: machine_instrs -> str + ls_insts = block_from_machine_instrs(machine_instrs) + lsa = LinearScanAllocator() + return lsa.emit(ls_insts) # 输出 str,MachineInstr 到此结束 +``` + +`_generate_riscv_linear()` 返回 `str` 后进入 `_run_asm_passes()`。要插入 SPEC 的调度 pass,需要: +1. 让 linear-scan 返回 `list[MachineInstr]`(如 3.1 所述) +2. 在 `_generate_riscv_linear` 内部(而非 `_run_asm_passes` 中)增加调度调用 +3. 修改 `_generate_riscv_dag` 做同样修改 + +SPEC 没有提供上述改造的具体代码级方案,只做了架构层面的描述。 + +### 3.3 MachineInstr 当前的语义不足以支持 SPEC 的需求(结构性 gap) + +**SPEC 需要的 InstructionView**(6.2 节): +```text +InstructionView + ordered_operands + explicit_defs / explicit_uses + implicit_defs / implicit_uses + memory_effect / memory_address + control_effect / target +``` + +**当前 MachineInstr**(`machine_types.py:134-142`): +```python +class MachineInstr: + op: MachineOp + dst: Optional[MachineOperand] = None + src1: Optional[MachineOperand] = None + src2: Optional[MachineOperand] = None + comment: str = "" +``` + +差距不仅是"字段不足",而是 MachineInstr 的语义角色本身就是错的(store 的 dst 实际上是值来源)。要适配 SPEC,需要先重构 MachineInstr(SPEC 4.1 也承认这一点),但这与 SPEC 自己的"调度器不应阻塞于 MachineInstr 重构"主张矛盾。 + +### 3.4 当前调度器位于 text-level 的实现债 gap + +SPEC 提出的新调度位置(post-RA/pre-emission)在架构上正确,但当前代码中: + +1. `_asm_parser.py:216-227` 的 `parse_asm()` 提供完整的 `ParsedAsmLine`(label、opcode、operands、comment、directive 分类) +2. `_asm_parser.py:245-278` 的 `classify_def_use()` 已经比 `inst_scheduler.py` 自己的 `parse_instructions()` 更完善 + +这意味着当前代码的文本解析基础已经比 SPEC 假设的更好。SPEC 说"当前汇编 parser 无法可靠处理 store/branch",但 `_asm_parser` 的 `classify_def_use`(第 245 行)已经正确处理了 `_NON_DEF_OPCODES`(第 130 行),包括 store、branch、jump。 + +SPEC 对 text-level 路径的批评有道理,但低估了它的成熟度。 + +--- + +## 4. 文档内容的完整性质疑 + +### 4.1 线性扫描分配器的处理过于简单 + +SPEC 只在 4.3 节和验收标准 15.3 中提到了 linear-scan: + +> "在 linear-scan 返回结构化结果之前,配置验证必须拒绝 `--reg-alloc linear --schedule`,不得静默跳过调度或回退到文本调度。" + +这是"禁止使用"而非"改造方案",但: +1. Linear-scan 是默认 allocator +2. 没有提供任何改造 linear-scan 的方案或工作量估计 +3. 拒绝组合意味着 SPEC 的 V1 必须默认切换 allocator 或修改默认配置 + +### 4.2 `InstructionView` 的实现来源不清晰 + +SPEC 6.2 节提出了 `InstructionView`,但: +- 未说明 adapter 的具体实现位置(新文件还是复用 `machine_types.py`) +- 未讨论 `implicit_defs`/`implicit_uses` 如何从当前 `MachineInstr` 推断(call 的 clobber 集合需要 ABI 知识) +- 未说明 `memory_address` 的 `structured base/offset/symbol` 如何从 dst/src1 字段解析(`sw` 的地址分散在 dst 和 src1 中) + +这些问题在详细设计中通过 opcode 角色模板解决,但 SPEC 将这些问题推迟到"先重构 MachineInstr"阶段。 + +### 4.3 验证失败处理策略过于严格 + +SPEC 第 11 节的失败处理策略是 **fail-stop**: + +| 场景 | SPEC 行为 | 对教育编译器的适用性 | +|---|---|---| +| 依赖图有环 | 内部错误,编译失败 | ❌ 可选优化不应中断编译 | +| 验证发现依赖违规 | 编译失败,不输出部分结果 | ❌ 用户只损失到一个优化 | +| terminator 后存在同块指令 | 结构错误,编译失败 | ⚠️ 属于实现错误,可在开发期暴露 | +| 未知 opcode | barrier + 诊断 | ✅ | + +教育/实验编译器的失败处理哲学应该是**安全降级**而非 **fail-stop**。SPEC 的策略复制了 LLVM/GCC 的生产质量要求,但不符合 ScratchV 的项目定位。 + +### 4.4 微架构模型的参数定义缺少具体化 + +SPEC 8 节定义了 `MicroArchitectureModel` 的接口形状和一大批 latency 表,但: + +1. 未定义与 `PipelineCycleEstimator` 的共享契约 +2. `result_latency` 与 `min_issue_distance` 的关系未明确定义 +3. 未讨论 forwarding 如何影响模型(flush 和 stall 的区别) +4. 单发射 baseline 参数中 `beq=1`、`j=0`、`jal=0`、`ret=0` 的值为 0 表示什么语义不清晰 + +相反,详细设计 8.2 节的 opcode 表更完整地给出了 latency、resource、issue_gap、memory effect 四维信息,并且对伪指令约束(8.3 节)有专门讨论。 + +--- + +## 5. SPEC 与详细设计的对比定位 + +| 定位 | SPEC | 详细设计 | +|---|---|---| +| **文档角色** | 架构决策记录(ADR) | 工程实现说明书 | +| **更适合的阶段** | 长期架构规划(V2+) | V1 立即实现 | +| **最大贡献** | 诊断当前代码缺陷 | 提供可直接编码的数据结构和伪代码 | +| **最大问题** | 缺少集成方案细节 | 缺少架构一致性分析 | + +SPEC 最适合作**项目的架构愿景文档(roadmap)**——定义"最终要建成什么样",但不作为 V1 实现的唯一指南。详细设计更适合作为**V1 实现的技术规格**。 + +对比详细设计缺少什么?详细设计缺少 SPEC 第 16 节的"当前设计评议"——它对当前代码的 12 条缺陷分析是任何实现开始时都应该读的。如果详细设计的 V1 实现不读 SPEC 16 节,就可能重复当前调度器已经犯过的错误。 + +--- + +## 6. SPEC 中可从详细设计吸收的部分 + +如果 SPEC 要更新到 V0.2,建议吸收以下内容: + +1. **具体数据类定义**(详细设计 7 节):`DependencyKind`、`OpcodeSchedulingInfo`、`DAGNode`、`ScheduleConfig` 等 +2. **集成位置妥协**:接受 V1 在 `_run_asm_passes()` 集成的现实,标注为"phase 1"而非终态 +3. **局部回退策略**:接受而非禁止,作为 `--schedule-strict` 的可选强化 +4. **具体测试用例**:详细设计 19.1 节的 20+ 个测试用例矩阵 +5. **项目计划**:12 周实施计划 + 8 个独立提交建议 + +--- + +## 7. 结论 + +### 7.1 是否建议按 SPEC 实现? + +**否。不建议将 SPEC 作为 V1 实现的唯一指南。** + +SPEC 在**架构分析**上是卓越的——它对当前代码的 8 条缺陷诊断(16.2 节)精确无比。但在**工程可行性**上存在三个难以逾越的 gap: + +1. 默认 allocator(linear-scan)不产生 MachineInstr +2. 当前 pass 编排不支持 post-RA/pre-emission 插入 +3. MachineInstr 本身的语义重构是前置依赖 + +### 7.2 建议的实际使用方式 + +| 用途 | 价值 | 说明 | +|---|---|---| +| **当前代码缺陷清单** | 极高 | 16.2 节可直接作为重构 task list | +| **正确性不变量** | 高 | 第 10 节的 11 条不变量是验收标准的基础 | +| **V2 架构愿景** | 中 | 调度位置前移应进入 backlog;但不宜在 V1 实现 | +| **组件设计参考** | 中 | 第 5 节的 7 组件分解可指导代码结构 | +| **具体实现指南** | 低 | 缺少可直接编码的数据结构和伪代码 | + +### 7.3 核心建议 + +1. **以详细设计为 V1 实现主体**,但吸收 SPEC 16.2 节的缺陷诊断作为实现前必须修复的 pre-condition +2. **SPEC 的 11 条不变量**应翻译为详细设计验证器的测试契约 +3. **调度位置前移**列入 V2 backlog,依赖 MachineInstr 语义重构和 linear-scan 输出改造完成 +4. **SPEC 本身**建议补充:linear-scan 改造方案、CompilerDriver 级别的集成伪代码、InstructionView 的具体 adapter 实现位置 + +--- + +*Review 生成日期:2026-07-28* +*评审范围:18-指令调度器SPEC.md v0.1* +*代码基线:ScratchV main HEAD* +*参考对比:18-指令调度器-详细设计-joska.md* diff --git "a/docs/topic-18/18-\346\214\207\344\273\244\350\260\203\345\272\246\345\231\250\344\273\243\347\240\201\350\257\264\346\230\216.md" "b/docs/topic-18/18-\346\214\207\344\273\244\350\260\203\345\272\246\345\231\250\344\273\243\347\240\201\350\257\264\346\230\216.md" new file mode 100644 index 0000000..cffe13b --- /dev/null +++ "b/docs/topic-18/18-\346\214\207\344\273\244\350\260\203\345\272\246\345\231\250\344\273\243\347\240\201\350\257\264\346\230\216.md" @@ -0,0 +1,60 @@ +# 指令调度器:当前实现说明 + +状态:与 2026-09-16 review 迭代后的实现对应。文档导航及历史方案差异见 [README](README.md)。 + +## 调度器处理什么 + +当前 `--schedule` 接收寄存器分配之后生成的汇编字符串。编译器按汇编窥孔优化、常量合并、指令调度、汇编美化的顺序执行已启用的 pass。 + +调度入口将文本解析为不可变的 `SchedInst`,记录原始行、寄存器读写、访存与浮点副作用,再按区域建立依赖 DAG。调度结束后移动原始文本行,保留标签、指示行、注释以及换行符。传给下一个 pass 的仍是汇编字符串。 + +| 模块 | 职责 | +|---|---| +| `schedule_semantics.py` | 精确描述支持的指令形式;无法确认时给出固定原因 | +| `schedule_model.py` | 指令延迟、资源占用、发射阻塞、顺序估算和延迟敏感性检查 | +| `schedule_verify.py` | 验证身份、置换、边界、读取来源、最终写值、副作用及依赖顺序 | +| `inst_scheduler.py` | 区域切分、依赖图、列表调度、收益判定、文本重写和报告 | + +## 哪些顺序不能变 + +依赖图保留 RAW(先写后读)、WAR(先读后覆盖)、WAW(两次写同一寄存器)、访存、浮点状态和控制流顺序。WAW 的成本还考虑较早的长延迟写入完成时间。 + +`div/divu/rem/remu` 保持相对所有其他指令的位置,避免提前除法造成目标相关的负优化。这个约束也适用于直接调用 `InstructionScheduler.build_dag()` 的使用者。 + +标签、指示行、独立注释和空行切分区域;`label: instruction` 同行固定。默认不跨基本块、不放宽内存别名、不把 `max` 等多指令展开当成一条指令。 + +`.pushsection/.popsection/.previous` 会保存和恢复段状态;无法恢复时报告诊断,直到显式切换段。指定非执行 flags 或显式命名为数据段的内容保持原位。字符串里的 `;`、`#`、反斜杠和 `.` 不会被误当成汇编布局操作。真正的宏、条件汇编、数字分支偏移等仍整份保留,并定位到命中行。 + +## 如何判断值得重排 + +主模型为 `scratchv-conservative-v2`:单发射,整数乘法 4 拍、除法/余数 33 拍;除法按保守的全局发射阻塞估算。它不是完整的 Rocket 模型。 + +候选顺序必须先通过正确性验证,然后同时满足: + +1. 主模型的完成周期严格减少。 +2. 另一组整数延迟(乘法 3、除法/余数 66)下也严格减少。 + +第二项只检查收益对延迟参数的敏感程度,不模拟完整的 SiFive E76,也没有建模其全部发射、旁路或缓存机制。调用方显式传入的延迟覆盖值在两组检查中都优先生效。 + +只在主模型下有收益时,区域状态为 `model_sensitive`,保留原序。报告给出两组候选估算,不把被拒绝候选的收益计入 `saved_cycles`。固定 load-use 示例仍为 5→4 拍;review 的 100 条指令负例因敏感性检查保留原序。 + +乘除法参数参考 LLVM 的 [Rocket 调度表](https://github.com/llvm/llvm-project/blob/llvmorg-18.1.8/llvm/lib/Target/RISCV/RISCVSchedRocket.td) 和 [SiFive7 调度表](https://github.com/llvm/llvm-project/blob/llvmorg-18.1.8/llvm/lib/Target/RISCV/RISCVSchedSiFive7.td)。性能验收由独立的 llvm-mca A/B 完成;总体验收通过仍可能有个别负例。 + +## 如何看报告 + +`CompileResult.stats["schedule"]` 包含周期、覆盖率、未建模指令按 opcode 的计数、区域大小、敏感性回退数和逐区域明细。`--schedule-report` 的多行文本保存在该字典的 `report` 字段,CLI 单独打印;warnings 用于需要注意的覆盖缺口或失败诊断。 + +源指令计数采用同一范围:执行段里的指令行,包括未知 opcode 和伪指令;不计标签、显示标签、指示行及数据段。伪指令的一行计为一条源指令,不等于编码后的机器指令数。 + +## 失败与兼容性 + +默认验证失败会恢复整区并警告。严格模式抛出 `ScheduleError`,编译器不会覆盖已有输出。关闭调度时不会调用调度入口。 + +推荐使用 `schedule_assembly(text, ScheduleConfig(...))`。旧接口仍保留,但有以下约束: + +- `SchedInst` 已不可变;读写寄存器从 opcode 和操作数重新计算,构造时提供的 def/use 不能覆盖语义。 +- `parse_instructions()` 返回的区域标识必须保留;跨区域调用 `build_dag()` 会报错。 +- `machine_instrs_from_scheduled()` 仅支持不会丢失信息的简单形式,对内存、控制和未知形式显式报错。 +- `InstructionScheduler.report()` 是兼容接口;编译器及完整汇编使用 `ScheduleResult.report()`。 + +linear 分配器现在通过 `LsInstruction.target` 保存控制流目标,发射真实标签及目标操作数。条件分支和 store 的第一个操作数按读取处理。机器指令对象会将整数寄存器源位置上的常量 0 规范为 `zero`;立即数位置仍保留 0,其他常量不会被擅自解释为寄存器。 diff --git "a/docs/topic-18/18-\346\214\207\344\273\244\350\260\203\345\272\246\345\231\250\345\256\236\347\216\260\350\256\241\345\210\222.md" "b/docs/topic-18/18-\346\214\207\344\273\244\350\260\203\345\272\246\345\231\250\345\256\236\347\216\260\350\256\241\345\210\222.md" new file mode 100644 index 0000000..c88a14d --- /dev/null +++ "b/docs/topic-18/18-\346\214\207\344\273\244\350\260\203\345\272\246\345\231\250\345\256\236\347\216\260\350\256\241\345\210\222.md" @@ -0,0 +1,277 @@ +# Topic 18 指令调度器实现计划 + +状态:2026-09-11 的历史 V1 实施计划,主体已经实现;2026-09-16 完成 review 迭代。本文后续“当前代码”“本次”等描述指当时的基线,现状请看 [代码说明](18-指令调度器代码说明.md) 和 [迭代报告](18-指令调度器Review迭代报告.md)。 + +与计划的后续差异:上游默认分配器现为 `greedy`;本轮修复了 linear 的分支目标发射;成本模型和收益门槛已增加保守除法约束与延迟敏感性检查。本文保留原计划内容,不能据此判断现有实现仍未完成。 + +核对基线:ScratchV `7356c4d`;本地 LLVM `14e941a0566a`。 + +## 1. 已经确定的范围 + +本次先交付一个可以实际使用的指令调度器:在一小段没有跳转的汇编里交换指令顺序,让处理器少等几拍,同时保证程序结果不变。 + +用户已确认两项决定: + +1. **保留现有汇编入口**,完成安全重排、验证和收益报告。默认寄存器分配器继续使用 `linear`,本次不改造寄存器分配器。 +2. **自检失败时恢复该段原序并警告**;测试使用严格模式,发现错误就失败。 + +`--schedule` 仍然默认关闭。算法接收独立的指令描述,汇编解析留在入口层;将来改为直接接收 `MachineInstr` 时,可以更换入口而继续使用依赖分析和排序算法。 + +本文按上述决定调整原设计中的“禁止文本调度”和“所有验证失败都中断编译”要求。原设计和 review 保留,便于追踪。 + +## 2. 当时基线中需要修复的问题(历史记录) + +| 发现 | 对实现的影响 | +|---|---| +| `compiler.py::_run_asm_passes` 解析后重新拼汇编;实际复现了函数标签丢失,`.text`、`.globl` 被去掉点并参与排序 | 输出必须移动原始行,不能重新拼操作码和操作数 | +| 默认 `linear` 分配器返回字符串,其他分配路径也最终汇入汇编处理入口 | V1 在现有入口接入即可覆盖这些路径,不必先改分配器 | +| 现有依赖图没有“读完旧值才能覆盖”的约束,也没有内存和控制边界 | 必须先补正确性规则,才能讨论优先级 | +| 现有周期估算只是把各条指令的延迟相加;实测换序前后都是 5 | 改为按照实际顺序模拟等待,否则无法衡量收益 | +| `_asm_parser.py` 已保存原始行、标签、指令和注释 | 复用解析结果,但其 `classify_def_use` 仍不足以作为完整指令说明表 | +| 现有 benchmark 使用 `r0_...` 等非物理寄存器,还把 store 当成写寄存器 | 性能用例需要换成合法输入,不能直接沿用结果 | +| `tests/test_cnn_pipeline.py::scratchv_output` 执行的是 IR | 这组数值测试不能单独证明调度后的机器指令正确 | + +review 提到的 `18-指令调度器-详细设计-joska.md` 未在当前仓库找到;本计划依据已提供的两份文档和实际代码制定,不假定缺失文档的内容。 + +## 3. 用一个例子说明最终行为 + +假设每拍最多开始一条指令,读内存的结果要等两拍才能使用: + +```asm +# 原序 +lw t0, 0(a0) +add t1, t0, t2 +addi t3, t3, 1 +ret + +# 调度后 +lw t0, 0(a0) +addi t3, t3, 1 +add t1, t0, t2 +ret +``` + +| 周期,从 0 开始 | 原序 | 调度后 | +|---|---|---| +| 0 | 开始读内存 | 开始读内存 | +| 1 | 等待 `t0` | 先算不依赖 `t0` 的 `addi` | +| 2 | `add` | `add` | +| 3 | `addi` | `ret` | +| 4 | `ret` | 已结束 | + +这个小模型中,4 条指令的完成时间由 5 拍变成 4 拍。没有增加指令,也没有插入 `nop`。这是模型里的收益,不代表某款真实处理器一定快 20%。 + +## 4. 哪些地方允许换顺序 + +先沿汇编行扫描,把程序切成可以单独处理的小段。这里的“小段”就是调度区域,比整个函数小,也可能比一个基本块小。 + +| 遇到的内容 | V1 处理方式 | +|---|---| +| 普通整数计算、已完整描述的浮点计算、常规 load/store | 允许参与排序,前提是不违反依赖 | +| 标签、section、汇编指示行,如 `.align`、`.cfi_*` | 保持原位,指令不能跨过去;数据段不参与调度 | +| 行尾注释 | 跟着所属指令一起移动 | +| 独立注释行、空行 | 第一版作为分隔,原样保留,避免猜测注释属于谁 | +| `label: instruction` 同行 | 整行固定;识别其中的控制效果,下一行从新区域开始 | +| 条件分支、无条件跳转、返回 | 固定在区域末尾,不能靠“降低优先级”保证位置 | +| 条件分支后紧接一个 `j` | 两条都固定,保持次序;后面的代码另起区域 | +| `call`、带返回地址的 `jal/jalr`、系统调用、fence、CSR、原子指令 | 固定并截断区域,任何指令都不能跨过去 | +| 未知操作码、操作数格式不匹配、宏或无法完整解析的行 | 原样保留并截断区域,报告原因 | +| `auipc`、`la`、含重定位或当前位置表达式的指令 | 第一版固定,避免换位置后计算出不同地址 | +| 一行包含多条语句,或布局依赖无法可靠判断 | 跳过受影响范围;若无法判断范围则整份汇编保留原序并诊断 | + +输出使用 `raw` 原始文本和原始行身份。保留原始换行符及末尾换行;不调用会统一格式的 `to_asm()` 重写全文。同一条文本出现多次时也必须保留每次出现的独立身份。 + +可识别的小立即数 `li`、`mv`、`nop` 按已知单条展开处理。大立即数 `li`、`li.d`、自定义多条展开伪指令等先固定,不把“汇编的一行”误当作“处理器的一条指令”。是否扩展它们的成本模型,以真实用例覆盖率决定。 + +对没有 section 声明的独立汇编片段,入口默认按代码片段处理。对有 section 的输入,显式跟踪当前段;不确定是否可执行时保持原序。 + +## 5. 怎么判断两条指令不能交换 + +给每条指令准备一份小说明:读哪些寄存器、写哪些寄存器、是否访问内存、是否跳转、是否允许移动。说明表按操作码和操作数格式定义,不再猜“第一个参数总是写入位置”。 + +| 约束 | 通俗解释 | 示例 | +|---|---|---| +| 先写后读,RAW | 要先算出结果,后面的指令才能使用 | `lw t0, ...` 必须在读取 `t0` 的 `add` 前面 | +| 先读后写,WAR | 旧值还没被读取,不能先覆盖 | `add t1, t0, t2` 必须在后面的 `li t0, 7` 前面 | +| 先写后写,WAW | 两次写入不能颠倒,否则最后留下的值会变 | 两次给 `t0` 赋值,保持先后 | +| 内存顺序 | 暂时不能证明地址是否重叠,因此读写内存彼此不换顺序 | load/load、load/store、store/load、store/store 都保序 | +| 控制顺序 | 还没做完本段计算,不能提前跳走 | 本段正文都在末尾分支之前 | + +内存保序并不意味着 load 后面必须立即接下一次访存;无关的计算仍然可以放进等待空档。 + +实现时按原序扫描一次,记录每个寄存器最近一次写入,以及从那次写入之后有哪些读取。根据这份记录添加“谁必须在谁前面”的连线,不逐对比较所有指令。处理一条既读又写同一寄存器的指令时,先处理读取,再处理写入,并排除指向自身的连线。 + +其他必须明确的细节: + +- `t0/x5`、`fp/s0/x8` 是同一个寄存器;浮点寄存器 `fa0/f10` 等也要统一身份。 +- `zero/x0` 永远是零:写入不产生新值,读取不等待写入。`lw x0, ...` 仍是一次访存,不能删除或忽略内存效果。 +- store 读取“要存的值”和“地址基址”;branch 读取判断条件;return 隐式读取 `ra`。 +- call 完全隔开两侧,因此 V1 无须为了跨调用移动而模拟整套调用约定。不能把未知 `jalr` 当普通计算。 +- 浮点计算可能还会更新异常标志、读取舍入方式。V1 对有此副作用的浮点操作增加保守顺序链,CSR 操作固定;无法完整描述时固定,不能只看浮点寄存器。 +- 项目可能输出非标准地址写法。先收集各分配路径的真实输出,为标准写法和确认过的项目写法分别定义模板;格式不明时固定,不凭正则猜测。 +- 多种原因产生同一条连线时,合并连线,等待距离取最大值,并保留全部原因供诊断。 + +普通内存访问的相对顺序全部保留;对于汇编中已丢失的 volatile 等上游信息,不能声称可以重新恢复。第一版的语义保证针对正常执行的后端程序,不承诺保留任意手写代码的计时观察、故障时刻或依赖固定代码地址的行为。 + +## 6. 怎么挑下一条指令 + +区分两种“准备好了”: + +1. 前面必须排的指令都已经排了。 +2. 它需要的结果已经算出来,而且对应计算单元现在空闲。 + +只有同时满足两点,才是当前可以开始执行的候选。比如 `lw` 已经排进结果,不等于下一拍就能使用它读出的值。 + +候选较多时,先选“后面还有一长串计算在等它”的指令;平局先比较最早可开始周期,再比较原位置,保证同样输入每次排出同样结果。 + +实现约定: + +- 从后向前计算后续等待链的长度;每个节点取自身完成延迟与所有“边距离 + 后继长度”的最大值,避免重复计算自身延迟。 +- 读取结果的边使用结果可用延迟;WAR、WAW、内存和控制等纯顺序边在本次单发射模型中使用 1 拍,不让它们平白等待整条除法的执行时间。 +- 使用剩余前驱计数、按就绪时间排序的等待队列,以及按优先级排序的候选队列。按资源分类维护候选,避免每发射一条就重扫剩下的所有指令。 +- 没有能开始的指令时,直接推进到下一个结果就绪或资源释放的周期。仅记等待,不生成 `nop`。 +- 分支作为固定的末尾节点计入数据依赖和成本。条件分支加 `j` 的静态比较固定采用相同的顺序口径,不把两条都计入的数字当作实际分支路径耗时。 +- 单段超过 1024 条可执行指令时,默认跳过并报告;压力测试可以显式调整阈值。 + +构图与验证目标为 `O(V+E)`,排队目标为 `O((V+E) log V)`;代码中用普通循环或迭代算法,避免长依赖链触发 Python 递归深度限制。 + +## 7. 怎么算收益 + +新增一个明确命名的教学模型 `scratchv-single-issue-v1`,调度和调度报告共用它。模型把三个不同概念分开:多久能用到结果、下一条什么时候能开始、某个计算单元会忙多久。 + +| 类别 | 结果可用时间 | 资源规则 | +|---|---|---| +| 整数计算、已确认的单条伪指令 | 1 拍 | 每拍可开始下一条 | +| load | 2 拍 | 访存每拍可开始下一条 | +| store | 没有寄存器结果;本地完成成本按 1 拍 | 占一个发射位置,内存仍保序 | +| multiply | 3 拍 | 乘法可连续启动,无关计算可穿插 | +| divide/remainder | 16 拍 | 同一个整数除法单元占满 16 拍,其他资源可继续工作 | +| 浮点加减、乘法 | 单精度 3/4 拍;双精度 4/5 拍 | 独立浮点资源;另受浮点副作用保序约束 | +| 浮点除法/平方根 | 单精度 12 拍;双精度 16 拍 | 共用浮点除法/平方根资源,直到完成才释放 | +| 固定分支/跳转/返回 | 1 拍 | 占一个发射位置 | + +其他允许移动的操作必须在说明表中给出明确成本,不能通过未知操作码默认 1 拍悄悄混入。所有资源仍共享“全局每拍最多开始一条”的限制。 + +按给定顺序模拟:每条指令的开始时间,是“上条指令开始后一拍、输入结果就绪时间、资源空闲时间”三者中的最大值。寄存器跟踪按每次写入的身份区分,避免覆盖同名寄存器时混淆旧值与新值。最终成本取所有指令完成时间的最大值,包括末尾尚未用到的长延迟结果。 + +原序和候选顺序必须经过同一个独立的顺序估算函数;不直接拿调度算法内部记录的时钟当作验证结果。 + +应用规则:先验证候选正确,再比较成本。**只有候选成本严格更低才采用**;持平或更差都保留原序。报告同时列出原序、候选和实际采用结果,拒绝候选时不能把候选收益写成已获得收益。 + +跨区域的输入就绪情况在该局部模型中统一采用固定假设,屏障不冒充已精确建模的指令。报告给出覆盖率和各区域成本之和,明确这是“局部静态模型估算”,不是整程序动态运行周期;局部变好也不等于所有真实硬件和跨块执行都一定变好。 + +与已有 `PipelineCycleEstimator` 的关系: + +- 它的 load 延迟 1 指的是执行阶段占用,调度需要的 2 指的是读取结果的等待距离,两者不是同一个字段,不能机械统一数值。 +- 第一版调度器和新顺序估算器共用新模型。原 `--cycle-stats` 五级流水线报告保留既有接口,标明其统计口径,不能混用两份数字计算调度收益。 +- 本次先不重写旧的五级流水线模拟器。两个模型完整合并属于后续工作;新模型内部禁止再复制延迟表。这是对原设计“本次同时统一全部估算器”的明确范围调整。 + +## 8. 自检和恢复原序 + +排序在临时的指令身份列表上进行,原始输入一直保留。候选通过检查后才替换对应区域。 + +自检至少包括:指令身份无增删重复、原始文本未改写、边界未跨越、所有依赖顺序成立、内存顺序成立、末尾跳转次序成立。还要独立比较每次寄存器读取在原序和新序中读到的是不是同一次写入,避免验证器完全依赖可能有遗漏的构图结果。 + +确定性通过重复运行测试验证;不把运行耗时纳入“统计每次完全一样”的要求。再次调度是否仍然改变也加入测试,但不声称“只接受更低成本”本身就能数学上保证一次收敛。 + +| 情况 | 默认行为 | 严格模式 | +|---|---|---| +| 未知指令、无法描述的格式、区域超限 | 保留原序并诊断 | 同样安全跳过,不把不支持当算法错误 | +| 候选无收益 | 保留原序,报告原因 | 同默认 | +| 构图出现环、漏指令、依赖违反、估算内部失败 | 恢复整个受影响区域并警告 | 本次编译失败 | +| 无法可靠恢复区域边界 | 恢复整份调度输入并警告 | 本次编译失败 | +| 用户配置不支持,例如 LLVM 后端开启此调度器 | 返回配置错误 | 同默认 | + +只捕获调度模块能够解释的错误,不使用吞掉所有程序错误的通用 `except Exception: pass`。严格失败时不写出部分输出,也不覆盖已有输出文件。 + +## 9. 准备修改哪些文件 + +以下是建议文件边界,函数细节可在编码时调整,但每项职责保留独立测试入口。 + +| 文件 | 改动 | +|---|---| +| `scratchv/backend/inst_scheduler.py` | 生产入口 `schedule_assembly`、切分区域、构图、排队与模块 CLI;保留必要旧 API 兼容层 | +| 新增 `scratchv/backend/schedule_semantics.py` | 指令说明表、寄存器身份和地址模板、只读指令视图 | +| 新增 `scratchv/backend/schedule_model.py` | 模型配置、资源规则、独立的给定顺序成本估算 | +| 新增 `scratchv/backend/schedule_verify.py` | 检查置换、边界、读写关系与原文保留 | +| `scratchv/backend/_asm_parser.py` | 优先直接复用;确需增强时增加兼容的辅助接口,覆盖现有消费者回归 | +| `scratchv/compiler.py` | 在现有汇编处理位置调用新入口,传回统计与警告,配置校验与严格失败处理 | +| `scratchv/main.py` | 保留 `--schedule`,增加 `--schedule-strict`、`--schedule-report` | +| `scratchv/backend/__init__.py` | 暴露新入口,处理已有导出接口 | +| `tests/test_inst_scheduler*.py` | 新的精确行为测试、执行对比和编译器入口测试 | +| `benchmarks/bench_inst_scheduler.py` | 使用合法物理寄存器和正确读写关系,输出模型收益、覆盖率及编译耗时 | +| topic 文档及本目录实现说明 | 修正示例算法和“已完成”描述,补充用法、范围与实测结果 | + +建议的生产接口: + +```python +schedule_assembly(asm_text, config) -> ScheduleResult +# result.asm_text:最终采用的完整汇编 +# result.stats:模型、覆盖率、每段原序/候选/最终成本、移动数、跳过数 +# result.diagnostics:源行号、原因、是否恢复原序 +# result.changed:最终是否真的改变了指令次序 +``` + +核心算法只接收指令视图,返回身份顺序;不认识文件路径、命令行或 `CompilerDriver`。`ScheduleConfig` 默认包含模型名、严格模式关闭、最大区域 1024。 + +编译顺序保持:已有汇编窥孔优化 → 常量合并 → **调度与自检** → 美化/计数。调度收益的“原序”是紧接调度之前的输入。 + +统计写入 `CompileResult.stats["schedule"]`,每次编译独立生成。`--schedule-report` 把人能读懂的报告写到 stderr,机器可读数据仍在结果对象中;它与严格选项都要求同时启用 `--schedule`。 + +模块命令行也使用同一完整汇编入口。旧 `parse_instructions/build_dag` 等单段 API 不再承担整份汇编处理;兼容层不得默默丢弃边界。当前无生产调用的 `machine_instrs_from_scheduled` 不能继续把未知操作码变成 `MV`,应显式拒绝不支持的转换并说明弃用方向。 + +## 10. 分步实施与验收 + +| 步骤 | 工作 | 做到什么算完成 | +|---|---|---| +| 1. 建立基线 | 准备 pytest 等测试依赖;保存默认输出;把标签丢失和估算无变化两个问题写成回归测试;收集各代码生成路径的操作码/地址写法 | 能复现旧错误,未启用调度的输出有明确基线 | +| 2. 安全读取与分段 | 指令说明、别名、原始行保存、标签/调用/跳转/未知行边界 | 原序往返不改变一个字节;结构测试通过 | +| 3. 正确依赖 | RAW/WAR/WAW、内存保序、浮点副作用、末尾分支、重复边合并 | 精确断言需要哪些边、每条边等几拍;读写自身不产生环 | +| 4. 调度和收益 | 后续等待链、两类队列、资源占用、独立成本估算、严格改善才采用 | 本文例子完成时间 5→4;输入已好或候选变差时保持原序 | +| 5. 自检与回退 | 独立读取来源检查、原文/身份/边界检查、默认回退和严格失败 | 注入漏指令、错误交换和循环依赖,分别验证回退、诊断和严格失败 | +| 6. 接入实际编译 | 编译器入口、CLI、结果统计、模块 CLI、旧 API 与 benchmark 迁移 | 默认 linear 路径确实进入新调度器;选项和报告端到端有效 | +| 7. 执行与回归 | 真实汇编器和模拟执行;合法随机区域;循环、spill、分支、调用;CNN/Conv 产物覆盖审计 | 执行结果无差异,跳过项清晰可见;完成项目要求的回归测试 | +| 8. 交付说明 | 更新 topic、示例、LLVM 对照、收益与耗时记录 | 文档、CLI 和实现一致,报告可重现且没有把模型数字写成实测硬件加速 | + +步骤按依赖顺序推进,每步可以单独评审。优先完成整数和常规内存样例,再在同一说明表框架下补齐浮点和生产路径覆盖;不能把未实现的类别默默算作已经支持。 + +最低测试矩阵: + +- RAW、WAR、WAW 各自独立的反例;同一寄存器多次读写;store 的值/地址;别名和零寄存器。 +- 连续标签、多函数、数字标签、同行标签、数据段、指示行、空行、行尾与独立注释、换行保留。 +- 条件分支、条件分支加跳转、返回、调用前后的寄存器修改、未知/系统/原子指令、重定位与多条展开伪指令。 +- 浮点与整数间转换、浮点别名、异常标志顺序、浮点 load/store;无法建模的形式能安全跳过。 +- load-use 精确周期、两次除法竞争同一资源、长延迟末尾结果、持平和退化拒绝、空段/单指令/超大段。 +- 默认配置、greedy/naive、DAG 指令选择、其他汇编优化组合、LLVM 后端拒绝、CLI 和同一 driver 连续编译。 +- 合法随机区域使用固定 seed,比较调度前后的输出寄存器和数据内存;地址均有效,不引入随机未定义行为。 + +验证器检查不是形式证明,必须配合执行对比。对执行对比使用 Clang 的 RISC-V 汇编器及 QEMU,测试包装代码把需要比较的寄存器/数据写出。带调用的程序比较业务结果与约定输出,不把随代码布局变化的链接地址字节混入结果。 + +仓库内置编码器存在项目特有语法与不完整指令支持,因此“能编码”不能单独作为验收。遇到原序已经不能被真实汇编器接受的生产样例,要报告现有后端问题;不得让前后两份无效代码都失败却计作调度通过。 + +CNN/Conv 需要区分三个检查:调度入口是否真的处理了其汇编、实际执行的汇编输出是否一致、模型估算是否变化。现有 IR 数值对比只能作上游回归。standalone 路径直接生成二进制,接入它涉及分支地址等额外工作,本次不自动扩展到该路径,也不宣称优化了它。 + +## 11. LLVM 具体看哪里 + +借鉴的是处理问题的方法,不移植整套 LLVM 框架,也不照抄某个 CPU 的参数。 + +| 本地 LLVM 位置 | 看什么 | 在本项目里的用途 | +|---|---|---| +| `llvm/lib/CodeGen/MachineScheduler.cpp`,`isSchedBoundary`,约 757 行 | 遇到调用或其他边界,停止跨越调度 | 区域切分规则 | +| `llvm/lib/CodeGen/ScheduleDAGInstrs.cpp`,`addPhysRegDataDeps` / `addPhysRegDeps`,约 247/303 行 | 根据真实寄存器读写建立三类依赖,跳过常量寄存器 | 我们的读写说明表和构图 | +| `llvm/lib/CodeGen/PostRASchedulerList.cpp`,`ListScheduleTopDown`,约 544 行 | 分清“依赖已排好”和“到这个周期才能执行”,检查资源后推进周期 | 最直接的排序主循环参考 | +| `llvm/lib/Target/RISCV/RISCVSchedRocket.td`,约 14/33/64 行起 | 发射宽度、结果延迟、计算单元占用分别描述 | 我们的目标模型字段拆分 | + +上述路径相对于 `/home/master/dev/llvm-project`。LLVM 里反依赖边可以是 0,用于同拍多发射;本项目固定每拍一条,因此保序边采用 1 是本模型的选择,不是通用硬件规律。LLVM 某些目标会要求插 `nop`,本项目暂不采用该行为。 + +review 的一个事实需要纠正:`MachineScheduler` 不等于“只能在寄存器分配之后运行”。本地 `MachineScheduler.cpp` 约 376 行明确写着普通 pass 在分配之前,另有 `PostMachineScheduler`。本次已经选定的晚期调度可以参考后者和 `PostRASchedulerList`,不需要因此改变本次入口决定。 + +LLVM 也把指令说明与具体优化算法分开,这支持本计划使用独立指令视图与说明表的方向。参考:[LLVM 官方代码生成文档](https://llvm.org/docs/CodeGenerator.html#target-description-classes)。 + +## 12. 当前已核实的环境与后续讨论点 + +已运行一个最小 RISC-V 程序,Clang 编译/链接和 `qemu-riscv32` 执行均返回 0。因此真实汇编与执行测试有可用工具链,无需为了参考调度代码先编译整个 LLVM。 + +当前 Python 有 numpy;没有 pytest、onnx、tinyfive、onnxruntime。尝试运行现有调度测试得到 `No module named pytest`,尚未获得测试基线。依赖准备列入实施第一步,不把测试无法启动写成测试通过。 + +后续讨论优先围绕实际样例推进:先共同走一遍“不能交换”的三个寄存器例子和本文 5→4 的周期表,再审看区域切分及说明表。上述分段规则、局部成本口径、旧五级估算器合并延期等是本计划的建议细节;用户目前明确确认的是第 1 节的入口范围与回退策略。 diff --git "a/docs/topic-18/18-\346\214\207\344\273\244\350\260\203\345\272\246\345\231\250\350\256\276\350\256\241\346\226\207\346\241\243.md" "b/docs/topic-18/18-\346\214\207\344\273\244\350\260\203\345\272\246\345\231\250\350\256\276\350\256\241\346\226\207\346\241\243.md" new file mode 100644 index 0000000..6c9bc9d --- /dev/null +++ "b/docs/topic-18/18-\346\214\207\344\273\244\350\260\203\345\272\246\345\231\250\350\256\276\350\256\241\346\226\207\346\241\243.md" @@ -0,0 +1,634 @@ +# Topic 18 指令调度器工程设计 + +- **状态**:结构化 V2 backlog,尚未实现;不是文本 V1 的现状说明 +- **版本**:0.1 +- **适用范围**:ScratchV RISC-V 后端,基本块内本地指令调度 +- **目标阶段**:寄存器分配之后、汇编发射之前 +- **目标读者**:后端实现者、代码审查者、性能验证负责人 +- **最后更新**:2026-07-25 + +> 2026-09-16 状态说明:本文是早期的 post-RA / pre-emission 设计,后续 V1 改为安全文本调度。文中的“当前实现”及源码行号指设计时的旧基线;完整依赖、原始行保真、独立验证等问题已在 V1 修复。当前行为与本设计差异见 [文档状态页](README.md),验收以 [迭代报告](18-指令调度器Review迭代报告.md) 为准。 + +## 1. 执行摘要 + +本设计采用**寄存器分配后、汇编发射前的结构化机器指令调度**:调度器直接处理已分配物理寄存器的 `MachineInstr`,按机器基本块划分调度区域,构建寄存器、内存和控制依赖图,执行确定性的关键路径优先列表调度,最后用独立验证器证明输出是安全的块内置换。 + +核心决策如下: + +| 决策 | 结论 | +|---|---| +| 调度阶段 | post-register-allocation、pre-assembly-emission | +| 调度粒度 | 单个机器基本块内;调用、屏障和未知指令可进一步切分调度区域 | +| 输入表示 | 结构化机器指令视图,不解析汇编字符串 | +| 寄存器依赖 | RAW、WAR、WAW,包含显式和隐式寄存器效果 | +| 内存依赖 | 第一版保守保持所有内存操作的原始相对顺序 | +| 控制约束 | 标签、directive 不参与调度;terminator 固定在块末尾 | +| 调度策略 | 关键路径高度优先,原始位置稳定打破平局 | +| 性能模型 | 与流水线估算器共享同一目标模型;估算必须对顺序敏感 | +| 安全策略 | 未知指令按 barrier 处理;验证失败时禁止静默回退到已重排结果 | +| 上线策略 | 默认关闭,显式 `--schedule` 启用,完成语义和性能验收前不改默认值 | + +该功能优化的是流水线停顿和 CPI,不减少程序语义所需的动态指令数。它不能直接解决 ScratchV 与 LLVM 的动态指令数差距,相关收益必须单独度量。 + +## 2. 问题定义 + +系统必须在不改变程序语义、控制流、汇编结构和指令集合的前提下,重排 RISC-V 机器基本块内的可移动指令,以减少由数据相关和功能单元可用性造成的流水线停顿。 + +当前 Topic 18 已有可选生产入口,但其生产路径在汇编文本生成后重新解析字符串,缺少完整依赖、基本块边界和结构保真约束。新设计必须把正确性建立在结构化机器指令和可验证不变量上,而不是建立在文本启发式规则上。 + +## 3. 目标与非目标 + +### 3.1 目标 + +1. 在 RISC-V 后端提供可选、确定、可验证的基本块内列表调度。 +2. 对物理寄存器建立完整的 RAW、WAR、WAW 依赖。 +3. 保留标签、directive、注释、分支目标和函数边界。 +4. 保证分支、跳转、返回等 terminator 位于基本块末尾。 +5. 对内存、调用、未知操作采用保守且明确的安全策略。 +6. 使用统一的目标微架构模型描述 latency、结果可用时间和资源约束。 +7. 对原始顺序和调度顺序执行同一套顺序敏感成本估算。 +8. 输出机器可读统计和可审计诊断,支持 CI、benchmark 和回归分析。 +9. 在默认关闭时保持现有 RISC-V 输出不变。 +10. 为 CNN/Conv 生产编译链提供端到端语义和性能验收。 + +### 3.2 非目标 + +1. 跨基本块全局调度、trace scheduling 或 superblock scheduling。 +2. 软件流水、循环展开或 modulo scheduling。 +3. 寄存器重命名以及消除 WAR/WAW 假依赖。 +4. 基于完整别名分析的激进内存重排。 +5. 精确模拟乱序、超标量桌面处理器。 +6. 修改指令选择语义、减少指令数量或进行窥孔优化。 +7. 在首个版本中默认启用调度。 +8. 复用 SelectionDAG 线性化器承担机器指令调度职责。 + +## 4. 架构决策 + +### 4.1 流水线位置 + +目标流水线为: + +```text +IR + → Instruction Selection + → Register Allocation + → Machine Basic-Block Partitioning + → Instruction Scheduling + → Schedule Verification + → Assembly Emission +``` + +调度器位于寄存器分配之后,原因如下: + +- 物理寄存器已经确定,RAW、WAR、WAW 和调用破坏集合可以精确建模。 +- 调度器不需要推测虚拟寄存器最终是否发生合并、spill 或重命名。 +- 输出仍是结构化 `MachineInstr`,标签、directive、注释和分支目标不会经历字符串 round-trip。 +- 调度结果不会被后续寄存器分配重新插入 spill 指令而失效。 +- 同一调度 pass 可以服务线性指令选择和 SelectionDAG 指令选择路径。 + +调度发生在汇编发射之前,避免当前文本解析路径丢失结构信息。当前编译器先完成选择、寄存器分配和 `AsmEmitter.emit()`,之后才调用 assembly passes;该位置必须前移到结构化机器指令阶段(`scratchv/compiler.py:386–473`)。 + +### 4.2 机器 CFG 与 IR CFG 的边界 + +现有 CFG 模块描述 IR 基本块、边和 terminator 摘要,但不持有 lowering 后的机器指令列表(`scratchv/analysis/cfg_builder.py:78–109, 231–343`)。机器调度不能直接把 IR CFG 当作调度输入,因为 instruction selection 会插入额外标签、分支、调用序列和伪指令。 + +本设计以**机器指令流中的结构化标签和 terminator**为机器基本块边界。IR CFG 可用于诊断和交叉验证,但不是机器调度的权威分区来源。 + +### 4.3 与寄存器分配器的契约 + +所有支持 `--schedule` 的寄存器分配路径必须返回结构化 `MachineInstr` 列表。当前 greedy/naive allocator 已返回该表示(`scratchv/backend/register_alloc.py:44–65, 100–130`),而 linear-scan 路径直接生成汇编文本(`scratchv/backend/regalloc_linear.py:383–416`)。 + +在 linear-scan 返回结构化结果之前,配置验证必须拒绝 `--reg-alloc linear --schedule`,不得静默跳过调度或回退到文本调度。 + +## 5. 组件 + +目标架构由七个职责单一的组件组成: + +| 组件 | 职责 | +|---|---| +| 后端调度 pass | 校验配置、接收已分配机器指令、编排分块、调度、验证和统计 | +| 机器基本块分区器 | 依据标签、terminator 和 barrier 把平坦机器指令流切成不可跨越的调度边界 | +| 指令语义描述器 | 为每个 opcode 提供显式/隐式 def-use、内存效果、控制效果、barrier 属性和目标资源 | +| 微架构模型 | 提供结果可用延迟、最小发射间隔、资源类别、流水化能力和 issue width | +| 依赖图构建器 | 构建寄存器、内存、控制和 barrier 依赖,并计算边的最小发射距离 | +| 列表调度引擎 | 计算关键路径高度,按就绪周期和优先级生成确定性调度 | +| 调度验证器 | 验证指令集合、边界、依赖顺序、terminator、确定性和统计一致性 | + +这些职责不能继续压缩到单个类中:指令语义决定正确性,微架构模型决定成本,调度策略决定顺序,验证器决定结果是否可接受。把它们分离后,可以独立测试依赖建模、目标模型和调度策略,也能避免算法代码重新实现 opcode 解析规则。 + +## 6. 数据与契约 + +### 6.1 输入前置条件 + +后端调度 pass 只接受满足以下条件的输入: + +1. backend 为 RISC-V。 +2. 寄存器分配已经完成,普通指令不再包含虚拟寄存器。 +3. 标签、directive 和控制目标仍保存在结构化机器指令中。 +4. 指令流没有 terminator 之后仍属于同一基本块的可执行指令。 +5. 每个 opcode 要么有完整语义描述,要么被显式分类为 barrier。 +6. 输入指令对象在本次调度期间视为不可变。 + +### 6.2 规范化机器指令视图 + +当前 `MachineInstr` 只有 `op`、`dst`、`src1`、`src2` 和 `comment`(`scratchv/backend/machine_types.py:130–152`)。这些字段是编码位置,不是可靠的语义角色:store 和 branch 的第一个位置并非寄存器定义,控制目标还被放在 `comment` 中(`scratchv/backend/instruction_select.py:159–163, 174–243`)。 + +调度核心必须消费一个只读的规范化视图,至少包含: + +```text +InstructionView + stable_id 块内稳定身份 + instruction 原始 MachineInstr 引用 + opcode 结构化 MachineOp + ordered_operands 编码顺序中的操作数 + explicit_defs 显式写寄存器 + explicit_uses 显式读寄存器 + implicit_defs 隐式写寄存器 + implicit_uses 隐式读寄存器 + memory_effect none / read / write / read_write / barrier + memory_address 结构化 base / offset / symbol;非内存为 none + control_effect none / branch / jump / return / call / barrier + target 结构化控制目标 + movable 是否允许进入调度区域 + original_index 原始块内位置 +``` + +规范化视图可以由 adapter 从现有 `MachineInstr` 生成,但调度算法不得直接根据 `dst` 字段或字符串前缀推断 def-use。adapter 必须把 `x0`/`zero`、`x1`/`ra`、`x2`/`sp`、`x8`/`s0`/`fp` 等寄存器别名归一为同一物理身份,并把内存地址与控制目标规范化为结构化字段;无法规范化时,该指令必须成为 barrier。 + +### 6.3 指令效果描述 + +指令语义描述器必须覆盖后端可生成的全部 `MachineOp`(`scratchv/backend/machine_types.py:27–95`),包括: + +- 整数、乘除、浮点运算的显式 def-use; +- load/store 的地址寄存器和值寄存器; +- branch 的条件寄存器与控制目标; +- `jal`、`jalr`、`call`、return 的 `ra` 和调用约定效果; +- stack pointer、frame pointer 和零寄存器的特殊规则; +- caller-saved 寄存器的 call clobber; +- label、section、directive 和未知 opcode 的 barrier 属性。 + +零寄存器写入不形成有效 definition;读取零寄存器不依赖任何前序写入。 + +### 6.4 机器基本块与调度区域 + +机器基本块由标签开始,在该块的最大 terminator bundle 结束后结束。单个 branch、jump、return 是一个 bundle;条件分支后紧随的无条件跳转作为同一 bundle 处理。标签和 directive 属于结构框架,不进入调度候选集。 + +基本块内部可继续按 barrier 切分为调度区域: + +```text +[label / directives] + region 0 + barrier or call + region 1 + terminator +``` + +第一版必须把以下项目视为 barrier: + +- call; +- fence、系统或 volatile 类操作; +- 语义描述不完整的 opcode; +- 明确要求保持位置的伪操作; +- 无法规范化的内存操作。 + +barrier 本身保持原位,任何指令不得跨越 barrier 移动。 + +### 6.5 调度结果 + +调度 pass 返回: + +```text +ScheduleResult + instructions 重组后的 MachineInstr 列表 + block_stats 每个块的原序/候选成本、应用/拒绝状态、移动数、依赖边计数 + diagnostics 跳过区域、barrier、未知 opcode、验证信息 + changed 是否存在至少一个指令位置变化 +``` + +输出必须复用原始机器指令对象或稳定身份,不得通过 opcode 文本重新构造指令。 + +### 6.6 状态生命周期 + +- 微架构模型是一次编译期间共享的不可变配置。 +- 指令语义表是只读定义。 +- 基本块、依赖图、ready queue、issue time 和验证状态均为单次 pass 的临时数据。 +- 调度器不得维护跨编译的可变全局状态。 +- 相同输入和相同模型必须产生相同输出与统计。 + +## 7. 依赖模型 + +### 7.1 寄存器依赖 + +对每个物理寄存器维护最近 definition 和自最近 definition 以来的 reads: + +| 类型 | 条件 | 方向 | 目的 | +|---|---|---|---| +| RAW | 前序写,后序读 | definition → use | 保证消费者在值可用后发射 | +| WAR | 前序读,后序写 | use → later definition | 防止写入提前覆盖旧值 | +| WAW | 前序写,后序写 | definition → later definition | 保证最终写入顺序 | + +RAW 边的最小发射距离由微架构模型决定。WAR、WAW 和纯顺序边至少要求后继处于后续发射槽。 + +隐式寄存器必须参与同一算法。call 对 caller-saved 集合、`ra`、参数/返回寄存器和内存的影响不能通过普通三操作数字段推断。 + +### 7.2 内存依赖 + +第一版缺少可靠机器级 alias metadata,因此采用保守策略: + +- 所有 load、store 和 read-write memory operation 按原始顺序串成 memory chain; +- 普通 ALU 指令可在 memory operation 之间移动,只要寄存器依赖允许; +- 指令不得跨 call、fence、volatile 或未知内存效果移动; +- 不基于字符串地址判断两个内存访问独立。 + +后续版本只有在获得结构化地址和别名证明后,才能放宽 load-load 或不相交 stack-slot 的顺序约束。 + +### 7.3 控制依赖 + +- label 和 directive 不进入 DAG。 +- terminator bundle 以固定顺序的 pinned sink 节点进入依赖/成本图,但不进入普通 ready queue。 +- 每个可移动 body 节点到 bundle 首节点建立控制边,保证 terminator 始终位于块末尾;bundle 内部建立顺序边。 +- terminator 的显式和隐式寄存器 uses 仍建立 RAW 依赖,使条件值和跳转寄存器在发射前就绪。 +- call 默认切断调度区域;第一版不允许指令跨 call 移动。 +- 条件分支加无条件跳转等连续 terminator 保持内部顺序,作为一个整体参与成本估算和验证。 + +### 7.4 重复边与环 + +同一前驱、后继之间的多条依赖合并为一条边,最小发射距离取最大值。由于所有约束从原序早位置指向晚位置,依赖图应当无环;发现环表示语义描述或构图实现错误,必须终止本次调度。 + +## 8. 微架构模型 + +微架构模型是调度器和周期估算器的唯一延迟来源。当前调度器和 5-stage estimator 各自维护 latency 表,且对 load、store 等操作的数值并不一致(`scratchv/backend/inst_scheduler.py:28–55`;`scratchv/backend/cycle_estimator.py:117–160`)。新实现不得继续复制模型。 + +第一版目标模型为单发射、顺序执行、支持 forwarding 的简单 RISC-V 5-stage pipeline,至少描述: + +```text +MicroArchitectureModel + issue_width + result_latency(opcode, producer_result) + min_issue_distance(producer, consumer, dependency_type) + resource_class(opcode) + resource_occupancy(opcode) + is_pipelined(opcode) + branch_policy +``` + +第一版 baseline 参数固定如下;它是确定性的调度模型,不代表任何具体硬件: + +| 指令类别 | 结果可用/占用规则 | +|---|---| +| 整数 ALU、move、immediate | 1 cycle | +| load | consumer 最小发射距离 2 cycles | +| store | 无结果;memory chain 最小顺序距离 1 cycle | +| multiply | latency 3,允许后续独立指令继续发射 | +| divide/remainder | latency 16,执行资源非流水化 | +| conditional branch/jump | latency 1,作为 pinned terminator | +| single-precision add/sub | latency 3 | +| single-precision multiply | latency 4 | +| single-precision divide/sqrt | latency 12,非流水化 | +| double-precision add/sub | latency 4 | +| double-precision multiply | latency 5 | +| double-precision divide/sqrt | latency 16,非流水化 | +| call、unknown、fence、volatile | barrier,不跨越调度 | + +约束: + +- 第一版 `issue_width = 1`。 +- load-use latency 必须反映数据到达时间,不等同于 opcode 的总执行周期。 +- 非流水化 div、rem、sqrt 等操作必须占用对应资源到完成。 +- 分支预测和 cache miss 不影响块内合法顺序;它们可用于报告,但不能成为不稳定的调度随机源。 +- cache 模型关闭时,原序和新序比较必须完全确定。 +- 所有性能数字标记为“模型估算”,不得表述为实际硬件周期。 + +现有 `PipelineCycleEstimator` 已包含 forwarding、结构冲突和多周期操作概念(`scratchv/backend/cycle_estimator.py:36–73, 467–516`),但仍基于汇编文本解析。实现阶段应抽取共享模型和结构化 instruction semantics,而不是让调度器调用文本 parser。 + +## 9. 调度算法 + +### 9.1 构图 + +对每个调度区域按原始顺序扫描: + +1. 为每条可移动指令创建普通节点,为 terminator bundle 创建固定顺序的 pinned sink 节点。 +2. 根据显式和隐式寄存器效果添加 RAW、WAR、WAW 边。 +3. 根据 memory chain 添加内存顺序边。 +4. 添加 body → terminator 控制边和 bundle 内部顺序边。 +5. 根据 barrier 规则限制区域边界。 +6. 合并重复边并校验边方向。 +7. 以 reverse topological order 计算关键路径高度。 + +关键路径高度定义为: + +```text +height(node) = max(edge.min_distance + height(successor)) +leaf height = model.result_latency(node) +``` + +实现必须先完成后继高度,再计算前驱高度;不允许只检查 priority 非零而不验证已知 DAG 的精确结果。 + +### 9.2 列表调度 + +调度器同时维护: + +- 每个节点剩余前驱数; +- 每个节点最早可发射周期; +- 当前周期可发射的 ready set; +- 目标资源的可用周期; +- 已选择的输出顺序和 issue cycle。 + +候选优先级按以下稳定顺序比较: + +1. 关键路径高度更大; +2. 最早可发射周期更早; +3. 原始位置更小。 + +伪代码: + +```text +while unscheduled nodes remain: + add newly dependency-ready nodes + candidates = nodes ready at current cycle and with available resources + + if candidates is empty: + current cycle = next readiness or resource-availability cycle + continue + + node = highest-priority candidate + issue node + record issue cycle + update successor readiness and resource availability +``` + +调度输出只改变指令顺序,不自动插入 `nop`。无法被独立指令覆盖的空周期记录为模型 stall,由报告展示。 + +### 9.3 原序与新序估算 + +原始顺序和调度顺序必须使用同一模型执行顺序敏感模拟: + +- 原序模拟严格按原始指令顺序发射; +- 新序模拟严格按调度结果发射; +- 两者都受 RAW readiness、资源占用和单发射约束; +- 指令数、模型配置、分支策略和 cache 配置必须一致。 + +只有该比较产生的差值可以作为“调度模型收益”。单纯累加 opcode latency 与顺序无关,不是可接受的调度评估。 + +### 9.4 应用策略 + +调度器先生成候选顺序,再比较原序与候选顺序的模型成本: + +- 候选成本严格更低时,应用候选顺序; +- 成本相等或更高时,保留原序并记录拒绝原因; +- 正确性验证在成本判断前后都不得被绕过; +- terminator bundle 和 barrier 不计为“无收益重排”的理由,它们始终保持结构位置。 + +该策略避免没有可证明模型收益的代码 churn,并使重复调度自然趋于幂等。 + +### 9.5 复杂度 + +对包含 `V` 条指令和 `E` 条依赖边的区域: + +- 语义提取:`O(V)`; +- 构图:`O(V + E)`,不扫描所有指令对; +- 关键路径:`O(V + E)`; +- 使用优先队列的列表调度:`O((V + E) log V)`; +- 内存占用:`O(V + E)`。 + +第一版调度区域大小保护阈值固定为 1024 条可执行指令,并允许测试或 benchmark 显式覆盖。超过阈值时保持原序并产生诊断,不允许退化为无界二次扫描;阈值调整必须基于编译耗时数据单独评审。 + +## 10. 正确性不变量 + +验证器必须在每个已调度基本块上证明以下条件: + +1. **置换不变量**:输出与输入包含完全相同的可执行指令身份,多一条或少一条都失败。 +2. **边界不变量**:指令不跨 function label、block label、directive、barrier 或 call 移动。 +3. **依赖不变量**:每条 RAW、WAR、WAW、memory 和 bundle 边在输出中保持拓扑顺序。 +4. **控制不变量**:terminator bundle 保持内部顺序并位于基本块末尾。 +5. **结构不变量**:标签、directive、注释和控制目标未被删除、重建或改写。 +6. **寄存器不变量**:调度后不出现虚拟寄存器;零寄存器规则保持有效。 +7. **确定性不变量**:相同输入和模型重复调度得到相同顺序和统计。 +8. **禁用不变量**:未启用 `--schedule` 时输出字节级不变。 +9. **报告不变量**:统计中的指令数、移动数和 block 数与真实输出一致。 +10. **应用不变量**:只有模型成本严格下降的候选顺序可以替换原序。 +11. **失败原子性**:任何验证失败都不得把部分调度结果写入最终输出。 + +## 11. 失败处理 + +| 情况 | 行为 | +|---|---| +| 不支持的 backend | 配置错误;拒绝启动调度 | +| linear allocator 尚未返回结构化结果 | 配置错误;不得文本回退 | +| 未知 opcode | 把该指令作为 barrier,保持原位并记录诊断 | +| 缺失语义但被标记为可移动 | 内部错误;本次编译失败 | +| 依赖图出现环 | 内部错误;本次编译失败 | +| terminator 后存在同块指令 | 结构错误;本次编译失败 | +| 验证器发现依赖或置换违规 | 本次编译失败;不输出部分结果 | +| 区域超过安全阈值 | 保持该区域原序,记录跳过原因 | +| 性能估算失败 | 保持原序;性能报告标记 unavailable 并记录诊断 | + +显式启用 `--schedule` 后,不允许捕获异常并静默输出未经确认的结果。未知操作可以安全跳过,但内部一致性失败必须暴露。 + +## 12. 可观测性 + +每次调度至少记录: + +- 访问、调度、跳过的函数数、基本块数和区域数; +- 输入指令数、输出指令数、移动指令数和最大位移; +- RAW、WAR、WAW、memory、barrier 边数量; +- 未知 opcode 和 barrier 命中次数; +- 原序/新序估算周期、stall 和 CPI; +- 验证结果与失败原因; +- 调度器自身耗时。 + +人类可读报告用于本地分析;结构化统计进入 `CompileResult.stats`,供 CI dashboard 和 benchmark 消费。性能报告必须同时展示模型名称和配置摘要,避免不同模型结果被直接比较。 + +## 13. 编译器集成 + +### 13.1 配置与 CLI + +- `--schedule`:启用 apply + verify。 +- 未传入时保持现有输出不变。 +- `backend != riscv` 时拒绝该选项组合。 +- 调度模型采用显式配置名,默认使用项目批准的 RISC-V 5-stage 模型。 +- 调度统计写入 `CompileResult.stats`,不依赖 stderr 文本解析。 + +### 13.2 结构化集成点 + +推荐的 Driver 逻辑: + +```text +machine_instrs = instruction_select(program) +allocated = register_allocate(machine_instrs) +if schedule: + allocated, schedule_stats = machine_schedule(allocated, target_model) +asm_text = emit_assembly(allocated) +``` + +核心调度器不依赖 `CompilerDriver`、argparse 或文件 I/O。Driver adapter 可以接入现有 pass 体系,但核心 API 必须保持机器层类型约束,不以通用 `Any → PassResult` 代替明确契约。 + +### 13.3 两条 instruction-selection 路径 + +线性 instruction selector 和 SelectionDAG selector 最终都必须汇合到“已分配 `MachineInstr` 列表”这一契约,再进入同一个 scheduler。`scratchv_dag.DAGScheduler` 的职责仍是 SelectionDAG 线性化,不承担物理机器指令重排。 + +### 13.4 与 cycle estimator 的关系 + +调度器和 estimator 共享: + +- opcode 语义; +- latency 与 forwarding 模型; +- 资源类别与流水化属性; +- 结构化寄存器 def-use。 + +两者不共享调度状态。estimator 是独立消费者,用于比较原序和新序;scheduler 不以 estimator 的内部 pipeline state 作为算法全局状态。 + +## 14. 测试策略 + +### 14.1 单元测试 + +1. 每个 `MachineOp` 的显式和隐式 def-use。 +2. 寄存器别名归一,以及 store、branch、call、return 和结构化 memory operand 的特殊语义。 +3. label、directive、单 terminator、双指令 terminator bundle、call 和 unknown opcode 的分区行为。 +4. RAW、WAR、WAW 精确边集合与边距离。 +5. memory chain 和 barrier 边。 +6. 已知 DAG 的精确关键路径高度。 +7. ready cycle、资源占用和稳定 tie-break。 +8. 空块、单指令块、仅 terminator 块和连续标签。 +9. 置换、边界、依赖、terminator 验证器。 +10. 相同输入重复调度的确定性。 + +测试必须断言精确顺序或精确依赖,不接受只检查长度、非空或 priority 大于零的弱断言。 + +### 14.2 属性测试 + +对随机生成的合法机器基本块验证: + +- 输出是输入指令身份的置换; +- 所有依赖边保持顺序; +- barrier 与 terminator 位置规则成立; +- 同一 seed 结果确定; +- 对已应用的调度结果再次调度不产生新的顺序变化。 + +### 14.3 集成测试 + +至少覆盖: + +- `CompilerConfig.schedule = False` 时输出不变; +- CLI `--schedule` 确实经过结构化 scheduler; +- greedy 和 naive allocator; +- linear allocator 的显式拒绝或结构化支持; +- 线性 instruction selection 与 SelectionDAG selection; +- LLVM backend 与 `--schedule` 的配置拒绝; +- 多函数、多基本块、循环、conditional branch、return 和 call; +- 调度结果可被真实 RISC-V assembler 接受。 + +### 14.4 语义测试 + +- 对原序和调度后汇编分别装配并执行; +- 在模拟器中比较寄存器、内存和程序输出; +- 覆盖 RAW、WAR、WAW、spill、stack access、call clobber 和控制流; +- 对 CNN/Conv 生产编译产物执行端到端等价验证; +- 任意语义差异都阻断上线。 + +### 14.5 性能测试 + +- 规范 load-use 示例必须展示独立指令填充 stall 的非零收益; +- 原序和候选顺序使用同一共享模型; +- 每个实际应用的区域,其候选模型成本必须严格低于原序;成本相等或更高时保持原序; +- 静态指令数和动态指令数不得因纯调度增加; +- 记录模型周期、模拟器周期、CPI、stall 分类和编译耗时; +- CNN、循环体和混合 ALU/memory block 必须进入持续 benchmark; +- 不以单个合成 block 的最好结果代表全局收益。 + +## 15. 验收标准 + +### 15.1 正确性门槛 + +- [ ] 调度发生在寄存器分配后、汇编发射前。 +- [ ] 生产路径不解析或重建汇编字符串。 +- [ ] 所有已启用后端 opcode 有语义描述或明确 barrier 策略。 +- [ ] RAW、WAR、WAW、memory 和 control 约束有精确测试。 +- [ ] 标签、directive、注释、控制目标和 terminator 完整保留。 +- [ ] 原序与调度序通过 assembler 和模拟器语义等价验证。 +- [ ] `--schedule` 关闭时输出字节级不变。 +- [ ] 任何验证失败不会输出部分调度结果。 + +### 15.2 性能门槛 + +- [ ] 周期估算对指令顺序敏感。 +- [ ] 规范 load-use 用例产生可解释的 stall 减少。 +- [ ] 每个实际应用区域的候选模型成本严格低于原序;相等或更高时保持原序。 +- [ ] 调度不增加静态或动态指令数。 +- [ ] CNN/Conv 与循环 corpus 的模拟器总周期不超过预先定义的回退阈值;每个回退用例单独记录并审查。 +- [ ] 调度器耗时进入 CI 统计,后端编译时间预算在基线后确定并固化。 + +### 15.3 工程门槛 + +- [ ] 调度结果确定且可重复。 +- [ ] 统计进入 `CompileResult.stats`,包含模型标识。 +- [ ] unknown opcode、超大区域和跳过原因可观测。 +- [ ] linear allocator 要么提供结构化输出,要么拒绝与调度组合。 +- [ ] 文档、实现、测试和 CLI 行为一致。 + +## 16. 当前设计评议 + +### 16.1 当前实现做对的部分 + +- `--schedule` 默认关闭,降低了未验收功能影响生产输出的风险。 +- latency model、DAG node 和 scheduler 有初步职责分离。 +- 相同优先级按原始 ID 打破平局,具备确定性意图。 +- instruction selection 和 greedy/naive allocator 已共享结构化 `MachineInstr`。 +- register allocator 在 label 处 flush 映射,说明标签天然承载机器块边界语义(`scratchv/backend/register_alloc.py:106–110, 193–205`)。 +- 项目已有更丰富的 5-stage cycle estimator,可作为共享目标模型的基础。 + +### 16.2 当前实现不满足目标设计的部分 + +- 生产调度发生在 `AsmEmitter` 之后,结构化信息已经丢失。 +- 汇编 parser 根据 operand 位置猜测 def-use,无法可靠处理 store、branch、call 和隐式寄存器。 +- 没有机器基本块分区,可能跨标签或控制转移调度。 +- 构图仅实现 RAW 和 WAW,缺失 WAR、memory 和 control 依赖。 +- branch/jump/return 没有 terminator 固定策略。 +- cycle report 只累加 opcode latency,与顺序无关。 +- 关键路径传播顺序存在静态可疑点,现有测试只检查 priority 大于零。 +- `machine_instrs_from_scheduled` 通过未知 opcode 回退到 `MV`,不符合 fail-safe 语义。 +- 调度器、cycle estimator 和汇编 parser 重复维护 opcode 分类与 latency。 +- 专项测试缺少生产集成、结构保真、语义等价和精确依赖断言。 + +原设计引用的 `18-指令调度器真实状态调研.md` 未随仓库提供;本节只保留设计时的问题记录,当前修复证据见 [迭代报告](18-指令调度器Review迭代报告.md)。 + +### 16.3 承重部分与偶然部分 + +**可保留的承重资产**: + +- 默认关闭的配置策略; +- 结构化 `MachineOp`/`MachineInstr`; +- Driver 的显式 opt-in 配置传播; +- 现有 cycle estimator 的流水线概念; +- 当前测试和 benchmark 作为回归起点。 + +**不应成为新设计约束的偶然形状**: + +- `SchedInst` 文本中间表示; +- 正则汇编 parser; +- post-emission assembly pass 位置; +- 独立重复的 latency 表; +- 把控制目标放入 comment 后再从文本恢复; +- 未使用的 MachineInstr 回写 helper; +- 当前弱断言测试结构。 + +## 17. 实现顺序 + +1. 定义共享 instruction semantics 和微架构模型。 +2. 建立规范化 `InstructionView`,覆盖全部现有 `MachineOp`。 +3. 实现机器基本块/调度区域分区器。 +4. 实现 RAW、WAR、WAW、memory 和 barrier 构图。 +5. 实现正确的 reverse-topological critical-path 计算。 +6. 实现确定性列表调度和顺序敏感原序/新序估算。 +7. 实现独立验证器与结构化统计。 +8. 在 greedy/naive 的 post-RA、pre-emission 位置接入。 +9. 使 linear allocator 返回结构化 `MachineInstr` 后接入同一 pass。 +10. 完成 assembler、模拟器、CNN/Conv 与持续性能验收。 + +任何实现步骤都不得绕过前一阶段的类型契约和验证门槛。 + +## 18. 后续校准事项 + +第一版实现没有未决架构选择:使用第 8 节固定的保守 5-stage baseline 即可开始编码。具体 RISC-V 核心、forwarding 路径、load-use 延迟和资源占用的硬件校准,不阻塞核心实现,但在宣称真实硬件收益或评审默认启用前必须完成。 + +内存别名、pre-RA pressure-aware scheduling、issue width 大于 1 和默认启用策略均属于后续独立设计,不阻塞第一版。 diff --git a/docs/topic-18/README.md b/docs/topic-18/README.md new file mode 100644 index 0000000..37126d6 --- /dev/null +++ b/docs/topic-18/README.md @@ -0,0 +1,35 @@ +# Topic 18 文档状态与当前实现 + +更新日期:2026-09-16。当前交付的是**默认关闭、汇编文本入口的 V1 调度器**;本次 review 迭代更新了成本模型和收益判定,模型版本为 `scratchv-conservative-v2`。模型版本 v2 不代表已经完成结构化调度器 V2。 + +## 文档导航 + +| 文档 | 状态与用途 | +|---|---| +| [代码说明](18-指令调度器代码说明.md) | 当前实现:输入输出、模型、收益判断、边界和兼容性 | +| [Benchmark](18-指令调度器Benchmark.md) | 当前验证命令、统计口径和 CI 入口 | +| [Review 迭代报告](18-指令调度器Review迭代报告.md) | 本次 F1–F16 的处理、前后对比、验收结果和遗留事项 | +| [分支评审原文](topic18-review.md) | 外部评审证据,保留原文;其中 T1–T7 属于对照分支 | +| [实现计划](18-指令调度器实现计划.md) | 2026-09-11 的历史实施计划,主体已落地 | +| [工程设计](18-指令调度器设计文档.md) | 结构化 post-RA / pre-emission 调度的 V2 backlog,尚未实现 | +| [SPEC 评审](18-指令调度器SPEC-Review.md) | 对早期设计和旧基线的历史讨论,不作为现有实现的验收清单 | + +## 当前行为与早期设计的差异 + +| 项目 | 当前 V1 | 结构化 V2 目标 | +|---|---|---| +| 调度位置 | 寄存器分配、汇编发射之后 | 寄存器分配之后、汇编发射之前 | +| pass 输入输出 | 汇编字符串;内部临时构建 `SchedInst` 与依赖 DAG | 结构化机器指令和基本块 | +| 验证失败 | 默认恢复整段并警告;`--schedule-strict` 下失败 | 早期设计要求统一 fail-stop,仍需重新评估 | +| 寄存器分配器 | 默认 greedy;linear、naive 也可接入文本调度 | 所有路径提供完整的结构化机器指令 | +| 收益判定 | 主模型与延迟敏感性检查都严格改善才应用 | 按目标 CPU 校准的模型 | +| Fast / BURR | 尚无策略注册或选择接口 | 独立设计;BURR 还需要分配前的寄存器压力信息 | + +## 保留的限制 + +- `--schedule` 默认关闭;局部模型周期和 llvm-mca 周期都不是硬件实测。 +- 静态指令数不变不能单独证明整个程序的动态指令数;本功能不作为缩小与 LLVM 动态指令数差距的证据。 +- `max`、非零字面量占据寄存器位置、多指令伪操作仍固定;不能照搬编码器把任意未知操作数映射为 x0 的宽松行为。 +- 宏、条件汇编、数字分支偏移等布局风险仍整份保留,并报告实际命中行。独立注释、空行、标签继续作为分隔。 +- 单个区域超过 1024 条时默认跳过,可通过 Python API 或调度器模块 CLI 设置上限;不自动分窗或跨基本块调度。 +- 旧文档提到的 `18-指令调度器SPEC.md`、`18-指令调度器真实状态调研.md`、`18-指令调度器-详细设计-joska.md` 未随仓库提供,不作为本版验收依赖。 diff --git a/docs/topic-18/review-iteration-results.json b/docs/topic-18/review-iteration-results.json new file mode 100644 index 0000000..20e5183 --- /dev/null +++ b/docs/topic-18/review-iteration-results.json @@ -0,0 +1,2936 @@ +{ + "date": "2026-09-16", + "baseline_commit": "035d180", + "candidate": "review iteration commit containing this report", + "llvm_mca_version": [ + "LLVM (http://llvm.org/):", + " LLVM version 22.1.8", + " Optimized build." + ], + "iterations": 1, + "scope": "LLVM static CPU models; no hardware timing or whole-program execution", + "before": { + "status": "failed", + "model": "scratchv-single-issue-v1", + "synthetic_summary": { + "rocket-rv32": { + "samples": 72, + "before": 103055, + "after": 102285, + "saved": 770, + "win": 25, + "tie": 9, + "loss": 38 + }, + "sifive-e76": { + "samples": 72, + "before": 175522, + "after": 175441, + "saved": 81, + "win": 28, + "tie": 5, + "loss": 39 + } + }, + "real_summary": { + "rocket-rv32": { + "samples": 9, + "before": 66, + "after": 57, + "saved": 9, + "win": 9, + "tie": 0, + "loss": 0 + }, + "sifive-e76": { + "samples": 9, + "before": 54, + "after": 48, + "saved": 6, + "win": 6, + "tie": 3, + "loss": 0 + } + }, + "coverage": { + "greedy": { + "files": 24, + "changed_files": 3, + "input_instructions": 211, + "modeled_instructions": 180, + "unmodeled_instructions": 31, + "coverage_ratio": 0.8530805687203792, + "mean_region_size": 2.0930232558139537, + "max_region_size": 10, + "instruction_count_preserved": true + }, + "linear": { + "files": 24, + "changed_files": 1, + "input_instructions": 199, + "modeled_instructions": 104, + "unmodeled_instructions": 95, + "coverage_ratio": 0.5226130653266332, + "mean_region_size": 1.8571428571428572, + "max_region_size": 5, + "instruction_count_preserved": true + } + } + }, + "after": { + "status": "passed", + "model": "scratchv-conservative-v2", + "synthetic_summary": { + "rocket-rv32": { + "samples": 72, + "before": 103055, + "after": 102105, + "saved": 950, + "win": 22, + "tie": 41, + "loss": 9 + }, + "sifive-e76": { + "samples": 72, + "before": 175522, + "after": 174608, + "saved": 914, + "win": 24, + "tie": 37, + "loss": 11 + } + }, + "real_summary": { + "rocket-rv32": { + "samples": 12, + "before": 87, + "after": 75, + "saved": 12, + "win": 12, + "tie": 0, + "loss": 0 + }, + "sifive-e76": { + "samples": 12, + "before": 70, + "after": 63, + "saved": 7, + "win": 7, + "tie": 5, + "loss": 0 + } + }, + "coverage": { + "greedy": { + "files": 24, + "changed_files": 3, + "input_instructions": 211, + "modeled_instructions": 187, + "unmodeled_instructions": 24, + "coverage_ratio": 0.8862559241706162, + "mean_region_size": 2.2261904761904763, + "max_region_size": 10, + "instruction_count_preserved": true + }, + "linear": { + "files": 24, + "changed_files": 3, + "input_instructions": 199, + "modeled_instructions": 175, + "unmodeled_instructions": 24, + "coverage_ratio": 0.8793969849246231, + "mean_region_size": 2.0833333333333335, + "max_region_size": 5, + "instruction_count_preserved": true + } + } + }, + "synthetic": [ + { + "size": 10, + "seed": 42, + "chains": 1, + "input_sha256": "eb01aeef9f141569b3ae4de5ffc4cddee0aa8a7e9b9662d5efeb03b8af505cc8", + "before": { + "rocket-rv32": { + "before": 47, + "after": 46, + "saved": 1 + }, + "sifive-e76": { + "before": 79, + "after": 79, + "saved": 0 + } + }, + "after": { + "rocket-rv32": { + "before": 47, + "after": 46, + "saved": 1 + }, + "sifive-e76": { + "before": 79, + "after": 79, + "saved": 0 + } + }, + "changed_after": true, + "sensitivity_rejected": 0 + }, + { + "size": 10, + "seed": 42, + "chains": 2, + "input_sha256": "1c40a5a86ca63515c145de34361888eb4ab47c565d7173ab9803a5df7d075565", + "before": { + "rocket-rv32": { + "before": 45, + "after": 42, + "saved": 3 + }, + "sifive-e76": { + "before": 75, + "after": 76, + "saved": -1 + } + }, + "after": { + "rocket-rv32": { + "before": 45, + "after": 45, + "saved": 0 + }, + "sifive-e76": { + "before": 75, + "after": 75, + "saved": 0 + } + }, + "changed_after": false, + "sensitivity_rejected": 1 + }, + { + "size": 10, + "seed": 42, + "chains": 3, + "input_sha256": "cfda5e2cddb94d7129b201bfe5c2147d9bc69507ee8c3c4e2f13bcc00da23d12", + "before": { + "rocket-rv32": { + "before": 73, + "after": 73, + "saved": 0 + }, + "sifive-e76": { + "before": 135, + "after": 136, + "saved": -1 + } + }, + "after": { + "rocket-rv32": { + "before": 73, + "after": 73, + "saved": 0 + }, + "sifive-e76": { + "before": 135, + "after": 135, + "saved": 0 + } + }, + "changed_after": false, + "sensitivity_rejected": 0 + }, + { + "size": 10, + "seed": 42, + "chains": 8, + "input_sha256": "14db641c619872818c6019aa4843b735744dbf5bb0210c29e1b4ebabdfc0f378", + "before": { + "rocket-rv32": { + "before": 74, + "after": 74, + "saved": 0 + }, + "sifive-e76": { + "before": 134, + "after": 134, + "saved": 0 + } + }, + "after": { + "rocket-rv32": { + "before": 74, + "after": 74, + "saved": 0 + }, + "sifive-e76": { + "before": 134, + "after": 134, + "saved": 0 + } + }, + "changed_after": false, + "sensitivity_rejected": 0 + }, + { + "size": 10, + "seed": 1, + "chains": 1, + "input_sha256": "3d097b42b2050279d5776a5ad80ef54b6017bee93befe7156f27cbe08ebd8a23", + "before": { + "rocket-rv32": { + "before": 44, + "after": 41, + "saved": 3 + }, + "sifive-e76": { + "before": 77, + "after": 74, + "saved": 3 + } + }, + "after": { + "rocket-rv32": { + "before": 44, + "after": 42, + "saved": 2 + }, + "sifive-e76": { + "before": 77, + "after": 74, + "saved": 3 + } + }, + "changed_after": true, + "sensitivity_rejected": 0 + }, + { + "size": 10, + "seed": 1, + "chains": 2, + "input_sha256": "574a7b67fe52e76a2244c9d367eef0e3203fb6f199e4f48cd889902d0c74fd25", + "before": { + "rocket-rv32": { + "before": 42, + "after": 42, + "saved": 0 + }, + "sifive-e76": { + "before": 73, + "after": 75, + "saved": -2 + } + }, + "after": { + "rocket-rv32": { + "before": 42, + "after": 42, + "saved": 0 + }, + "sifive-e76": { + "before": 73, + "after": 73, + "saved": 0 + } + }, + "changed_after": false, + "sensitivity_rejected": 0 + }, + { + "size": 10, + "seed": 1, + "chains": 3, + "input_sha256": "d72013736c55a116b3136b2532a633f61b9b26989b257f0968dec881962298db", + "before": { + "rocket-rv32": { + "before": 73, + "after": 73, + "saved": 0 + }, + "sifive-e76": { + "before": 137, + "after": 136, + "saved": 1 + } + }, + "after": { + "rocket-rv32": { + "before": 73, + "after": 73, + "saved": 0 + }, + "sifive-e76": { + "before": 137, + "after": 137, + "saved": 0 + } + }, + "changed_after": false, + "sensitivity_rejected": 0 + }, + { + "size": 10, + "seed": 1, + "chains": 8, + "input_sha256": "48dad7068a0a510ab951d56692bfbd4dbd309108a618f239eab4131d3015bb5c", + "before": { + "rocket-rv32": { + "before": 42, + "after": 42, + "saved": 0 + }, + "sifive-e76": { + "before": 71, + "after": 71, + "saved": 0 + } + }, + "after": { + "rocket-rv32": { + "before": 42, + "after": 42, + "saved": 0 + }, + "sifive-e76": { + "before": 71, + "after": 71, + "saved": 0 + } + }, + "changed_after": false, + "sensitivity_rejected": 0 + }, + { + "size": 10, + "seed": 2, + "chains": 1, + "input_sha256": "17daf5e8085ca4855c7cce273fcc4d34f5486f285177816b28fc8b9ee9fb46a0", + "before": { + "rocket-rv32": { + "before": 48, + "after": 43, + "saved": 5 + }, + "sifive-e76": { + "before": 79, + "after": 74, + "saved": 5 + } + }, + "after": { + "rocket-rv32": { + "before": 48, + "after": 45, + "saved": 3 + }, + "sifive-e76": { + "before": 79, + "after": 76, + "saved": 3 + } + }, + "changed_after": true, + "sensitivity_rejected": 0 + }, + { + "size": 10, + "seed": 2, + "chains": 2, + "input_sha256": "d57b4e171e1583a01f106e5604b77880813454ea8a4be1983aece172406f1eb4", + "before": { + "rocket-rv32": { + "before": 44, + "after": 42, + "saved": 2 + }, + "sifive-e76": { + "before": 74, + "after": 73, + "saved": 1 + } + }, + "after": { + "rocket-rv32": { + "before": 44, + "after": 44, + "saved": 0 + }, + "sifive-e76": { + "before": 74, + "after": 73, + "saved": 1 + } + }, + "changed_after": true, + "sensitivity_rejected": 0 + }, + { + "size": 10, + "seed": 2, + "chains": 3, + "input_sha256": "e1476347b4a2042a408480be3d4402252e571725c1edc0e38f299cff9f636340", + "before": { + "rocket-rv32": { + "before": 73, + "after": 78, + "saved": -5 + }, + "sifive-e76": { + "before": 137, + "after": 140, + "saved": -3 + } + }, + "after": { + "rocket-rv32": { + "before": 73, + "after": 73, + "saved": 0 + }, + "sifive-e76": { + "before": 137, + "after": 137, + "saved": 0 + } + }, + "changed_after": false, + "sensitivity_rejected": 0 + }, + { + "size": 10, + "seed": 2, + "chains": 8, + "input_sha256": "854701990867cff4c3225d550a85f3dd090dd795477a41332702cffc86a9e54d", + "before": { + "rocket-rv32": { + "before": 73, + "after": 73, + "saved": 0 + }, + "sifive-e76": { + "before": 135, + "after": 134, + "saved": 1 + } + }, + "after": { + "rocket-rv32": { + "before": 73, + "after": 73, + "saved": 0 + }, + "sifive-e76": { + "before": 135, + "after": 135, + "saved": 0 + } + }, + "changed_after": false, + "sensitivity_rejected": 0 + }, + { + "size": 50, + "seed": 42, + "chains": 1, + "input_sha256": "074190f122dc77a0f86153a2fb0308efd498cac30ceea20cccf9f0680729486f", + "before": { + "rocket-rv32": { + "before": 266, + "after": 248, + "saved": 18 + }, + "sifive-e76": { + "before": 451, + "after": 430, + "saved": 21 + } + }, + "after": { + "rocket-rv32": { + "before": 266, + "after": 256, + "saved": 10 + }, + "sifive-e76": { + "before": 451, + "after": 444, + "saved": 7 + } + }, + "changed_after": true, + "sensitivity_rejected": 0 + }, + { + "size": 50, + "seed": 42, + "chains": 2, + "input_sha256": "c48d50d29f29bc9b8cb7213bf61fc95e269aef1cec806a44d9ce853f0efefb7a", + "before": { + "rocket-rv32": { + "before": 248, + "after": 244, + "saved": 4 + }, + "sifive-e76": { + "before": 431, + "after": 427, + "saved": 4 + } + }, + "after": { + "rocket-rv32": { + "before": 248, + "after": 246, + "saved": 2 + }, + "sifive-e76": { + "before": 431, + "after": 431, + "saved": 0 + } + }, + "changed_after": true, + "sensitivity_rejected": 0 + }, + { + "size": 50, + "seed": 42, + "chains": 3, + "input_sha256": "c56a4f0b0c4ab24faa7f9f9407785a3986093717caf3e34f0090c7b00fa99862", + "before": { + "rocket-rv32": { + "before": 208, + "after": 212, + "saved": -4 + }, + "sifive-e76": { + "before": 355, + "after": 364, + "saved": -9 + } + }, + "after": { + "rocket-rv32": { + "before": 208, + "after": 208, + "saved": 0 + }, + "sifive-e76": { + "before": 355, + "after": 355, + "saved": 0 + } + }, + "changed_after": false, + "sensitivity_rejected": 1 + }, + { + "size": 50, + "seed": 42, + "chains": 8, + "input_sha256": "93bacfefcb88202e4b69edf97e7ea1a4e2a9bb272d4d3fa1e078c1b3e00a03b2", + "before": { + "rocket-rv32": { + "before": 211, + "after": 211, + "saved": 0 + }, + "sifive-e76": { + "before": 350, + "after": 353, + "saved": -3 + } + }, + "after": { + "rocket-rv32": { + "before": 211, + "after": 211, + "saved": 0 + }, + "sifive-e76": { + "before": 350, + "after": 350, + "saved": 0 + } + }, + "changed_after": false, + "sensitivity_rejected": 0 + }, + { + "size": 50, + "seed": 1, + "chains": 1, + "input_sha256": "0f8d254539cea6bfbd526bbee930270f1bf0ddfb2f4ae4b928c4d35d711d9de2", + "before": { + "rocket-rv32": { + "before": 287, + "after": 280, + "saved": 7 + }, + "sifive-e76": { + "before": 512, + "after": 497, + "saved": 15 + } + }, + "after": { + "rocket-rv32": { + "before": 287, + "after": 281, + "saved": 6 + }, + "sifive-e76": { + "before": 512, + "after": 506, + "saved": 6 + } + }, + "changed_after": true, + "sensitivity_rejected": 0 + }, + { + "size": 50, + "seed": 1, + "chains": 2, + "input_sha256": "40e591f26f63ed6c2adfc94b68a979f73fb5ca12d2b6404d2bdb8f9b0c66ecdd", + "before": { + "rocket-rv32": { + "before": 278, + "after": 275, + "saved": 3 + }, + "sifive-e76": { + "before": 494, + "after": 494, + "saved": 0 + } + }, + "after": { + "rocket-rv32": { + "before": 278, + "after": 278, + "saved": 0 + }, + "sifive-e76": { + "before": 494, + "after": 495, + "saved": -1 + } + }, + "changed_after": true, + "sensitivity_rejected": 0 + }, + { + "size": 50, + "seed": 1, + "chains": 3, + "input_sha256": "49068461e6491c7382c345e2c14f470f9bd0d0beb8685c7d8a5d46d51dc6a0e8", + "before": { + "rocket-rv32": { + "before": 212, + "after": 213, + "saved": -1 + }, + "sifive-e76": { + "before": 355, + "after": 364, + "saved": -9 + } + }, + "after": { + "rocket-rv32": { + "before": 212, + "after": 212, + "saved": 0 + }, + "sifive-e76": { + "before": 355, + "after": 363, + "saved": -8 + } + }, + "changed_after": true, + "sensitivity_rejected": 0 + }, + { + "size": 50, + "seed": 1, + "chains": 8, + "input_sha256": "c000a2ebd27e4e1a599c919f6842819502ddfcc18f0376f1631a7c188750a500", + "before": { + "rocket-rv32": { + "before": 213, + "after": 214, + "saved": -1 + }, + "sifive-e76": { + "before": 347, + "after": 347, + "saved": 0 + } + }, + "after": { + "rocket-rv32": { + "before": 213, + "after": 213, + "saved": 0 + }, + "sifive-e76": { + "before": 347, + "after": 347, + "saved": 0 + } + }, + "changed_after": false, + "sensitivity_rejected": 1 + }, + { + "size": 50, + "seed": 2, + "chains": 1, + "input_sha256": "a2a4cdc8c52ec79ad80ab5165d8ae9bd444fc518bda2fd920bff137ad53f9652", + "before": { + "rocket-rv32": { + "before": 198, + "after": 185, + "saved": 13 + }, + "sifive-e76": { + "before": 319, + "after": 305, + "saved": 14 + } + }, + "after": { + "rocket-rv32": { + "before": 198, + "after": 185, + "saved": 13 + }, + "sifive-e76": { + "before": 319, + "after": 306, + "saved": 13 + } + }, + "changed_after": true, + "sensitivity_rejected": 0 + }, + { + "size": 50, + "seed": 2, + "chains": 2, + "input_sha256": "ca5988bc5eae9291407091b963e21592348ca0e63b336d2545b7e020205573be", + "before": { + "rocket-rv32": { + "before": 182, + "after": 184, + "saved": -2 + }, + "sifive-e76": { + "before": 297, + "after": 299, + "saved": -2 + } + }, + "after": { + "rocket-rv32": { + "before": 182, + "after": 183, + "saved": -1 + }, + "sifive-e76": { + "before": 297, + "after": 294, + "saved": 3 + } + }, + "changed_after": true, + "sensitivity_rejected": 0 + }, + { + "size": 50, + "seed": 2, + "chains": 3, + "input_sha256": "5b8824268ed7f8038924ebc2313f7aa030c9ec1a1236553800cd0730a5c2e3c7", + "before": { + "rocket-rv32": { + "before": 244, + "after": 249, + "saved": -5 + }, + "sifive-e76": { + "before": 420, + "after": 434, + "saved": -14 + } + }, + "after": { + "rocket-rv32": { + "before": 244, + "after": 244, + "saved": 0 + }, + "sifive-e76": { + "before": 420, + "after": 426, + "saved": -6 + } + }, + "changed_after": true, + "sensitivity_rejected": 0 + }, + { + "size": 50, + "seed": 2, + "chains": 8, + "input_sha256": "8c50c66ad9176d51fef719305fdcac9817e781e6f05b3171918586b5ba87a34f", + "before": { + "rocket-rv32": { + "before": 176, + "after": 182, + "saved": -6 + }, + "sifive-e76": { + "before": 286, + "after": 289, + "saved": -3 + } + }, + "after": { + "rocket-rv32": { + "before": 176, + "after": 176, + "saved": 0 + }, + "sifive-e76": { + "before": 286, + "after": 286, + "saved": 0 + } + }, + "changed_after": false, + "sensitivity_rejected": 0 + }, + { + "size": 100, + "seed": 42, + "chains": 1, + "input_sha256": "37b541233c0f904faad121829cff4d796a0da4fd4c29fb353644ce5e5ce1ba27", + "before": { + "rocket-rv32": { + "before": 550, + "after": 514, + "saved": 36 + }, + "sifive-e76": { + "before": 954, + "after": 908, + "saved": 46 + } + }, + "after": { + "rocket-rv32": { + "before": 550, + "after": 530, + "saved": 20 + }, + "sifive-e76": { + "before": 954, + "after": 940, + "saved": 14 + } + }, + "changed_after": true, + "sensitivity_rejected": 0 + }, + { + "size": 100, + "seed": 42, + "chains": 2, + "input_sha256": "458aa2236ac82570e80998729d6ba4bf2f858f023cb669c366058db9a781dd39", + "before": { + "rocket-rv32": { + "before": 516, + "after": 507, + "saved": 9 + }, + "sifive-e76": { + "before": 914, + "after": 906, + "saved": 8 + } + }, + "after": { + "rocket-rv32": { + "before": 516, + "after": 517, + "saved": -1 + }, + "sifive-e76": { + "before": 914, + "after": 917, + "saved": -3 + } + }, + "changed_after": true, + "sensitivity_rejected": 0 + }, + { + "size": 100, + "seed": 42, + "chains": 3, + "input_sha256": "31a9c756dd15e42660ba930fc5020ee119eb2bcb3782cb6591abe99e8098a975", + "before": { + "rocket-rv32": { + "before": 449, + "after": 462, + "saved": -13 + }, + "sifive-e76": { + "before": 769, + "after": 788, + "saved": -19 + } + }, + "after": { + "rocket-rv32": { + "before": 449, + "after": 449, + "saved": 0 + }, + "sifive-e76": { + "before": 769, + "after": 769, + "saved": 0 + } + }, + "changed_after": false, + "sensitivity_rejected": 1 + }, + { + "size": 100, + "seed": 42, + "chains": 8, + "input_sha256": "4bcb200f54c0e1859a1202a781dedb8bf846a8262044340082c2d8f10a66bd8a", + "before": { + "rocket-rv32": { + "before": 574, + "after": 575, + "saved": -1 + }, + "sifive-e76": { + "before": 1014, + "after": 1031, + "saved": -17 + } + }, + "after": { + "rocket-rv32": { + "before": 574, + "after": 574, + "saved": 0 + }, + "sifive-e76": { + "before": 1014, + "after": 1014, + "saved": 0 + } + }, + "changed_after": false, + "sensitivity_rejected": 0 + }, + { + "size": 100, + "seed": 1, + "chains": 1, + "input_sha256": "3af1dcd1d0713d7f74cd17810dba1e671c13e0d1faa6e428cc3d99f45b2f0e6f", + "before": { + "rocket-rv32": { + "before": 441, + "after": 429, + "saved": 12 + }, + "sifive-e76": { + "before": 757, + "after": 726, + "saved": 31 + } + }, + "after": { + "rocket-rv32": { + "before": 441, + "after": 428, + "saved": 13 + }, + "sifive-e76": { + "before": 757, + "after": 742, + "saved": 15 + } + }, + "changed_after": true, + "sensitivity_rejected": 0 + }, + { + "size": 100, + "seed": 1, + "chains": 2, + "input_sha256": "9e4702d36e74e19362b6c768043ce5801c2ffc0bbef50d9bb002db8b5a72ef0f", + "before": { + "rocket-rv32": { + "before": 423, + "after": 425, + "saved": -2 + }, + "sifive-e76": { + "before": 726, + "after": 728, + "saved": -2 + } + }, + "after": { + "rocket-rv32": { + "before": 423, + "after": 426, + "saved": -3 + }, + "sifive-e76": { + "before": 726, + "after": 725, + "saved": 1 + } + }, + "changed_after": true, + "sensitivity_rejected": 0 + }, + { + "size": 100, + "seed": 1, + "chains": 3, + "input_sha256": "820c35cc8fc7ca7ca6012d868b70ec3302d194da7dec7985567a91ea85f5432d", + "before": { + "rocket-rv32": { + "before": 546, + "after": 555, + "saved": -9 + }, + "sifive-e76": { + "before": 965, + "after": 982, + "saved": -17 + } + }, + "after": { + "rocket-rv32": { + "before": 546, + "after": 546, + "saved": 0 + }, + "sifive-e76": { + "before": 965, + "after": 965, + "saved": 0 + } + }, + "changed_after": false, + "sensitivity_rejected": 1 + }, + { + "size": 100, + "seed": 1, + "chains": 8, + "input_sha256": "21a2f019bf329620a63c092a166ac28527cb4727c490cee4c514728522ebb36c", + "before": { + "rocket-rv32": { + "before": 420, + "after": 421, + "saved": -1 + }, + "sifive-e76": { + "before": 690, + "after": 716, + "saved": -26 + } + }, + "after": { + "rocket-rv32": { + "before": 420, + "after": 420, + "saved": 0 + }, + "sifive-e76": { + "before": 690, + "after": 690, + "saved": 0 + } + }, + "changed_after": false, + "sensitivity_rejected": 0 + }, + { + "size": 100, + "seed": 2, + "chains": 1, + "input_sha256": "0c191a92695d1eabc0a21be87fb9ca1fd59def12da8191ec65cb1d2bec5fecd8", + "before": { + "rocket-rv32": { + "before": 385, + "after": 362, + "saved": 23 + }, + "sifive-e76": { + "before": 633, + "after": 600, + "saved": 33 + } + }, + "after": { + "rocket-rv32": { + "before": 385, + "after": 364, + "saved": 21 + }, + "sifive-e76": { + "before": 633, + "after": 608, + "saved": 25 + } + }, + "changed_after": true, + "sensitivity_rejected": 0 + }, + { + "size": 100, + "seed": 2, + "chains": 2, + "input_sha256": "15bc1e9cc4dcf89a8ff09ac52a222156ff132cb5eeacb2f7827b542dedc38041", + "before": { + "rocket-rv32": { + "before": 362, + "after": 360, + "saved": 2 + }, + "sifive-e76": { + "before": 595, + "after": 591, + "saved": 4 + } + }, + "after": { + "rocket-rv32": { + "before": 362, + "after": 367, + "saved": -5 + }, + "sifive-e76": { + "before": 595, + "after": 594, + "saved": 1 + } + }, + "changed_after": true, + "sensitivity_rejected": 0 + }, + { + "size": 100, + "seed": 2, + "chains": 3, + "input_sha256": "11f5acc27a3cc47cdb608eaa7d77f5a316a35c631c914f1cf170c07f68510534", + "before": { + "rocket-rv32": { + "before": 452, + "after": 452, + "saved": 0 + }, + "sifive-e76": { + "before": 770, + "after": 786, + "saved": -16 + } + }, + "after": { + "rocket-rv32": { + "before": 452, + "after": 452, + "saved": 0 + }, + "sifive-e76": { + "before": 770, + "after": 770, + "saved": 0 + } + }, + "changed_after": false, + "sensitivity_rejected": 1 + }, + { + "size": 100, + "seed": 2, + "chains": 8, + "input_sha256": "aac915176bedd843681d613823037ab37309881334f0f7585c5ddfeaa5e4d634", + "before": { + "rocket-rv32": { + "before": 387, + "after": 391, + "saved": -4 + }, + "sifive-e76": { + "before": 636, + "after": 641, + "saved": -5 + } + }, + "after": { + "rocket-rv32": { + "before": 387, + "after": 387, + "saved": 0 + }, + "sifive-e76": { + "before": 636, + "after": 636, + "saved": 0 + } + }, + "changed_after": false, + "sensitivity_rejected": 0 + }, + { + "size": 200, + "seed": 42, + "chains": 1, + "input_sha256": "86634fadc51711b81acdbcef0908f8b6c3fffd34217e55c313d2ec303587b993", + "before": { + "rocket-rv32": { + "before": 1265, + "after": 1205, + "saved": 60 + }, + "sifive-e76": { + "before": 2242, + "after": 2165, + "saved": 77 + } + }, + "after": { + "rocket-rv32": { + "before": 1265, + "after": 1230, + "saved": 35 + }, + "sifive-e76": { + "before": 2242, + "after": 2214, + "saved": 28 + } + }, + "changed_after": true, + "sensitivity_rejected": 0 + }, + { + "size": 200, + "seed": 42, + "chains": 2, + "input_sha256": "719673c67e9ce691f27cd2b7594b38eeb3e7d794d68dea9f632193963da39023", + "before": { + "rocket-rv32": { + "before": 1194, + "after": 1195, + "saved": -1 + }, + "sifive-e76": { + "before": 2152, + "after": 2153, + "saved": -1 + } + }, + "after": { + "rocket-rv32": { + "before": 1194, + "after": 1197, + "saved": -3 + }, + "sifive-e76": { + "before": 2152, + "after": 2160, + "saved": -8 + } + }, + "changed_after": true, + "sensitivity_rejected": 0 + }, + { + "size": 200, + "seed": 42, + "chains": 3, + "input_sha256": "2a56464efee5eb3a19fdb184cf7c998f85fe9ccf0b43acde9910a54b9c5bf9ae", + "before": { + "rocket-rv32": { + "before": 958, + "after": 967, + "saved": -9 + }, + "sifive-e76": { + "before": 1661, + "after": 1685, + "saved": -24 + } + }, + "after": { + "rocket-rv32": { + "before": 958, + "after": 958, + "saved": 0 + }, + "sifive-e76": { + "before": 1661, + "after": 1661, + "saved": 0 + } + }, + "changed_after": false, + "sensitivity_rejected": 1 + }, + { + "size": 200, + "seed": 42, + "chains": 8, + "input_sha256": "d2d1d206ccf7476a47927366fe45fbd0b7e6fcb0aacf67f3f7db6b834e8542ff", + "before": { + "rocket-rv32": { + "before": 1092, + "after": 1106, + "saved": -14 + }, + "sifive-e76": { + "before": 1896, + "after": 1941, + "saved": -45 + } + }, + "after": { + "rocket-rv32": { + "before": 1092, + "after": 1092, + "saved": 0 + }, + "sifive-e76": { + "before": 1896, + "after": 1896, + "saved": 0 + } + }, + "changed_after": false, + "sensitivity_rejected": 0 + }, + { + "size": 200, + "seed": 1, + "chains": 1, + "input_sha256": "6aa6ae26e72ff249b1552a19e54b8a33858f16c0f781d855de72962501fae7a3", + "before": { + "rocket-rv32": { + "before": 945, + "after": 916, + "saved": 29 + }, + "sifive-e76": { + "before": 1628, + "after": 1579, + "saved": 49 + } + }, + "after": { + "rocket-rv32": { + "before": 945, + "after": 923, + "saved": 22 + }, + "sifive-e76": { + "before": 1628, + "after": 1604, + "saved": 24 + } + }, + "changed_after": true, + "sensitivity_rejected": 0 + }, + { + "size": 200, + "seed": 1, + "chains": 2, + "input_sha256": "4c37eaf10e28412b31c4a7df1f136603b66d9415b0ba2f2338388a81d7eaac6a", + "before": { + "rocket-rv32": { + "before": 909, + "after": 913, + "saved": -4 + }, + "sifive-e76": { + "before": 1569, + "after": 1574, + "saved": -5 + } + }, + "after": { + "rocket-rv32": { + "before": 909, + "after": 912, + "saved": -3 + }, + "sifive-e76": { + "before": 1569, + "after": 1576, + "saved": -7 + } + }, + "changed_after": true, + "sensitivity_rejected": 0 + }, + { + "size": 200, + "seed": 1, + "chains": 3, + "input_sha256": "9ff9c2250575618b7d4a004598341f2298874cfaf2042b0b5d44f5c1cf57c0f6", + "before": { + "rocket-rv32": { + "before": 939, + "after": 947, + "saved": -8 + }, + "sifive-e76": { + "before": 1613, + "after": 1640, + "saved": -27 + } + }, + "after": { + "rocket-rv32": { + "before": 939, + "after": 939, + "saved": 0 + }, + "sifive-e76": { + "before": 1613, + "after": 1613, + "saved": 0 + } + }, + "changed_after": false, + "sensitivity_rejected": 1 + }, + { + "size": 200, + "seed": 1, + "chains": 8, + "input_sha256": "6eaff03ebf4af6d24e2b461948642b68323f1ffca6cdd08b0da19cbdc9cfe4d0", + "before": { + "rocket-rv32": { + "before": 1022, + "after": 1051, + "saved": -29 + }, + "sifive-e76": { + "before": 1762, + "after": 1817, + "saved": -55 + } + }, + "after": { + "rocket-rv32": { + "before": 1022, + "after": 1022, + "saved": 0 + }, + "sifive-e76": { + "before": 1762, + "after": 1762, + "saved": 0 + } + }, + "changed_after": false, + "sensitivity_rejected": 0 + }, + { + "size": 200, + "seed": 2, + "chains": 1, + "input_sha256": "1db38307865858e33bee9093e723586df3fa301e5b77ad52a464d63378b58439", + "before": { + "rocket-rv32": { + "before": 867, + "after": 821, + "saved": 46 + }, + "sifive-e76": { + "before": 1453, + "after": 1395, + "saved": 58 + } + }, + "after": { + "rocket-rv32": { + "before": 867, + "after": 830, + "saved": 37 + }, + "sifive-e76": { + "before": 1453, + "after": 1415, + "saved": 38 + } + }, + "changed_after": true, + "sensitivity_rejected": 0 + }, + { + "size": 200, + "seed": 2, + "chains": 2, + "input_sha256": "009f5dae0b5ec897e14e4ebb53c9d9c6450ebc73d298eb17269b1dd5be19bfe1", + "before": { + "rocket-rv32": { + "before": 814, + "after": 814, + "saved": 0 + }, + "sifive-e76": { + "before": 1379, + "after": 1375, + "saved": 4 + } + }, + "after": { + "rocket-rv32": { + "before": 814, + "after": 818, + "saved": -4 + }, + "sifive-e76": { + "before": 1379, + "after": 1375, + "saved": 4 + } + }, + "changed_after": true, + "sensitivity_rejected": 0 + }, + { + "size": 200, + "seed": 2, + "chains": 3, + "input_sha256": "127702b7c36a85348f570638690e783edd2b0773e6437fae4fee7d760d754863", + "before": { + "rocket-rv32": { + "before": 904, + "after": 911, + "saved": -7 + }, + "sifive-e76": { + "before": 1539, + "after": 1554, + "saved": -15 + } + }, + "after": { + "rocket-rv32": { + "before": 904, + "after": 904, + "saved": 0 + }, + "sifive-e76": { + "before": 1539, + "after": 1539, + "saved": 0 + } + }, + "changed_after": false, + "sensitivity_rejected": 1 + }, + { + "size": 200, + "seed": 2, + "chains": 8, + "input_sha256": "972dd0027af9bb838d025454da52631f737c46ccd7ac69606d0530ec7876e18c", + "before": { + "rocket-rv32": { + "before": 773, + "after": 793, + "saved": -20 + }, + "sifive-e76": { + "before": 1270, + "after": 1308, + "saved": -38 + } + }, + "after": { + "rocket-rv32": { + "before": 773, + "after": 773, + "saved": 0 + }, + "sifive-e76": { + "before": 1270, + "after": 1270, + "saved": 0 + } + }, + "changed_after": false, + "sensitivity_rejected": 0 + }, + { + "size": 500, + "seed": 42, + "chains": 1, + "input_sha256": "8c0142a87a39fd6a9cddee3aa3b7ec231dce2bced495a1204e8f752fd6436102", + "before": { + "rocket-rv32": { + "before": 2702, + "after": 2572, + "saved": 130 + }, + "sifive-e76": { + "before": 4695, + "after": 4522, + "saved": 173 + } + }, + "after": { + "rocket-rv32": { + "before": 2702, + "after": 2615, + "saved": 87 + }, + "sifive-e76": { + "before": 4695, + "after": 4608, + "saved": 87 + } + }, + "changed_after": true, + "sensitivity_rejected": 0 + }, + { + "size": 500, + "seed": 42, + "chains": 2, + "input_sha256": "1e4c43f11ed94e6ab09efc7eb4b5f7159ef920dfccf7b3bd9b34bf44c44c5b98", + "before": { + "rocket-rv32": { + "before": 2565, + "after": 2564, + "saved": 1 + }, + "sifive-e76": { + "before": 4511, + "after": 4479, + "saved": 32 + } + }, + "after": { + "rocket-rv32": { + "before": 2565, + "after": 2564, + "saved": 1 + }, + "sifive-e76": { + "before": 4511, + "after": 4510, + "saved": 1 + } + }, + "changed_after": true, + "sensitivity_rejected": 0 + }, + { + "size": 500, + "seed": 42, + "chains": 3, + "input_sha256": "c282834d6d9f868ac0c87b475041913bb47c21169e521677bd03c655422a146f", + "before": { + "rocket-rv32": { + "before": 2263, + "after": 2271, + "saved": -8 + }, + "sifive-e76": { + "before": 3847, + "after": 3886, + "saved": -39 + } + }, + "after": { + "rocket-rv32": { + "before": 2263, + "after": 2263, + "saved": 0 + }, + "sifive-e76": { + "before": 3847, + "after": 3847, + "saved": 0 + } + }, + "changed_after": false, + "sensitivity_rejected": 1 + }, + { + "size": 500, + "seed": 42, + "chains": 8, + "input_sha256": "590dfcbb05e3c314ae7996f9b963c61e301491d6f7ec6d9f3122a936886bb286", + "before": { + "rocket-rv32": { + "before": 2518, + "after": 2532, + "saved": -14 + }, + "sifive-e76": { + "before": 4302, + "after": 4420, + "saved": -118 + } + }, + "after": { + "rocket-rv32": { + "before": 2518, + "after": 2518, + "saved": 0 + }, + "sifive-e76": { + "before": 4302, + "after": 4302, + "saved": 0 + } + }, + "changed_after": false, + "sensitivity_rejected": 0 + }, + { + "size": 500, + "seed": 1, + "chains": 1, + "input_sha256": "0508999594b87266dc70c04d46893d60c36a84dacdd624c702d23d2bd14c4b2d", + "before": { + "rocket-rv32": { + "before": 2241, + "after": 2152, + "saved": 89 + }, + "sifive-e76": { + "before": 3818, + "after": 3688, + "saved": 130 + } + }, + "after": { + "rocket-rv32": { + "before": 2241, + "after": 2175, + "saved": 66 + }, + "sifive-e76": { + "before": 3818, + "after": 3749, + "saved": 69 + } + }, + "changed_after": true, + "sensitivity_rejected": 0 + }, + { + "size": 500, + "seed": 1, + "chains": 2, + "input_sha256": "97d57f45f27e14a1379cbac2f475ca073d3efb4211ea7bea35ebf8087c4b9226", + "before": { + "rocket-rv32": { + "before": 2152, + "after": 2164, + "saved": -12 + }, + "sifive-e76": { + "before": 3665, + "after": 3667, + "saved": -2 + } + }, + "after": { + "rocket-rv32": { + "before": 2152, + "after": 2152, + "saved": 0 + }, + "sifive-e76": { + "before": 3665, + "after": 3672, + "saved": -7 + } + }, + "changed_after": true, + "sensitivity_rejected": 0 + }, + { + "size": 500, + "seed": 1, + "chains": 3, + "input_sha256": "10627c74ce02451eb973ad7d63148aa535bcab398ce37b3ea153f800ef3d1dda", + "before": { + "rocket-rv32": { + "before": 1900, + "after": 1916, + "saved": -16 + }, + "sifive-e76": { + "before": 3104, + "after": 3138, + "saved": -34 + } + }, + "after": { + "rocket-rv32": { + "before": 1900, + "after": 1900, + "saved": 0 + }, + "sifive-e76": { + "before": 3104, + "after": 3104, + "saved": 0 + } + }, + "changed_after": false, + "sensitivity_rejected": 1 + }, + { + "size": 500, + "seed": 1, + "chains": 8, + "input_sha256": "229aa8acbdfd13c328edc54baff951198cf1664df743efd9e4c8159ef01fe9e3", + "before": { + "rocket-rv32": { + "before": 1986, + "after": 2011, + "saved": -25 + }, + "sifive-e76": { + "before": 3218, + "after": 3316, + "saved": -98 + } + }, + "after": { + "rocket-rv32": { + "before": 1986, + "after": 1986, + "saved": 0 + }, + "sifive-e76": { + "before": 3218, + "after": 3218, + "saved": 0 + } + }, + "changed_after": false, + "sensitivity_rejected": 0 + }, + { + "size": 500, + "seed": 2, + "chains": 1, + "input_sha256": "6cb0b5cd05a844a6bccafa16d026bb4c21120109dcda4ab3b1432c239cfb1442", + "before": { + "rocket-rv32": { + "before": 2378, + "after": 2256, + "saved": 122 + }, + "sifive-e76": { + "before": 4021, + "after": 3871, + "saved": 150 + } + }, + "after": { + "rocket-rv32": { + "before": 2378, + "after": 2287, + "saved": 91 + }, + "sifive-e76": { + "before": 4021, + "after": 3929, + "saved": 92 + } + }, + "changed_after": true, + "sensitivity_rejected": 0 + }, + { + "size": 500, + "seed": 2, + "chains": 2, + "input_sha256": "6aa18a112d3e80f7b891b4d7c646f34d8e50dea560c8e7a615f9c608e6e46b29", + "before": { + "rocket-rv32": { + "before": 2215, + "after": 2230, + "saved": -15 + }, + "sifive-e76": { + "before": 3805, + "after": 3801, + "saved": 4 + } + }, + "after": { + "rocket-rv32": { + "before": 2215, + "after": 2229, + "saved": -14 + }, + "sifive-e76": { + "before": 3805, + "after": 3809, + "saved": -4 + } + }, + "changed_after": true, + "sensitivity_rejected": 0 + }, + { + "size": 500, + "seed": 2, + "chains": 3, + "input_sha256": "d10323d1583e39e3fa71074110ea705fca49c0ce232a97f6479500365d0a0107", + "before": { + "rocket-rv32": { + "before": 2359, + "after": 2369, + "saved": -10 + }, + "sifive-e76": { + "before": 4045, + "after": 4084, + "saved": -39 + } + }, + "after": { + "rocket-rv32": { + "before": 2359, + "after": 2359, + "saved": 0 + }, + "sifive-e76": { + "before": 4045, + "after": 4045, + "saved": 0 + } + }, + "changed_after": false, + "sensitivity_rejected": 1 + }, + { + "size": 500, + "seed": 2, + "chains": 8, + "input_sha256": "c2230694610b91c054de26cd107a1d8b199e9dfd22cf4a4c7d6245d70b99825e", + "before": { + "rocket-rv32": { + "before": 2146, + "after": 2165, + "saved": -19 + }, + "sifive-e76": { + "before": 3553, + "after": 3671, + "saved": -118 + } + }, + "after": { + "rocket-rv32": { + "before": 2146, + "after": 2146, + "saved": 0 + }, + "sifive-e76": { + "before": 3553, + "after": 3553, + "saved": 0 + } + }, + "changed_after": false, + "sensitivity_rejected": 0 + }, + { + "size": 1000, + "seed": 42, + "chains": 1, + "input_sha256": "da38d4af9d6548e79d38dcdb456605badd454782841e481ba9bf4ce68211bd7b", + "before": { + "rocket-rv32": { + "before": 4870, + "after": 4646, + "saved": 224 + }, + "sifive-e76": { + "before": 8342, + "after": 8033, + "saved": 309 + } + }, + "after": { + "rocket-rv32": { + "before": 4870, + "after": 4691, + "saved": 179 + }, + "sifive-e76": { + "before": 8342, + "after": 8153, + "saved": 189 + } + }, + "changed_after": true, + "sensitivity_rejected": 0 + }, + { + "size": 1000, + "seed": 42, + "chains": 2, + "input_sha256": "318d5344acab698590f19bb5e867d4776ee245ac2193f2f1b5f580df2a979e2d", + "before": { + "rocket-rv32": { + "before": 4626, + "after": 4635, + "saved": -9 + }, + "sifive-e76": { + "before": 7991, + "after": 7960, + "saved": 31 + } + }, + "after": { + "rocket-rv32": { + "before": 4626, + "after": 4617, + "saved": 9 + }, + "sifive-e76": { + "before": 7991, + "after": 7980, + "saved": 11 + } + }, + "changed_after": true, + "sensitivity_rejected": 0 + }, + { + "size": 1000, + "seed": 42, + "chains": 3, + "input_sha256": "e4f0a462cd08c26daf91fb73e161687ceb6c8a80ffcbc475359876cf94fc0363", + "before": { + "rocket-rv32": { + "before": 4496, + "after": 4528, + "saved": -32 + }, + "sifive-e76": { + "before": 7651, + "after": 7721, + "saved": -70 + } + }, + "after": { + "rocket-rv32": { + "before": 4496, + "after": 4496, + "saved": 0 + }, + "sifive-e76": { + "before": 7651, + "after": 7651, + "saved": 0 + } + }, + "changed_after": false, + "sensitivity_rejected": 1 + }, + { + "size": 1000, + "seed": 42, + "chains": 8, + "input_sha256": "372ee2bd80703fa2f61d56aca2b64b039d751e6f1cc1a38e2861583d85002f73", + "before": { + "rocket-rv32": { + "before": 4878, + "after": 4921, + "saved": -43 + }, + "sifive-e76": { + "before": 8295, + "after": 8522, + "saved": -227 + } + }, + "after": { + "rocket-rv32": { + "before": 4878, + "after": 4878, + "saved": 0 + }, + "sifive-e76": { + "before": 8295, + "after": 8295, + "saved": 0 + } + }, + "changed_after": false, + "sensitivity_rejected": 0 + }, + { + "size": 1000, + "seed": 1, + "chains": 1, + "input_sha256": "481f70f45faa6a5e17ed04a80139086e90150e0fdf9b46692757356f57bb4870", + "before": { + "rocket-rv32": { + "before": 4730, + "after": 4564, + "saved": 166 + }, + "sifive-e76": { + "before": 8114, + "after": 7862, + "saved": 252 + } + }, + "after": { + "rocket-rv32": { + "before": 4730, + "after": 4607, + "saved": 123 + }, + "sifive-e76": { + "before": 8114, + "after": 7977, + "saved": 137 + } + }, + "changed_after": true, + "sensitivity_rejected": 0 + }, + { + "size": 1000, + "seed": 1, + "chains": 2, + "input_sha256": "6c55ef8b2d7f9fb6e248953bd419576c77a23abc3e7b94cb3b16085f9afa0d9d", + "before": { + "rocket-rv32": { + "before": 4536, + "after": 4542, + "saved": -6 + }, + "sifive-e76": { + "before": 7796, + "after": 7801, + "saved": -5 + } + }, + "after": { + "rocket-rv32": { + "before": 4536, + "after": 4536, + "saved": 0 + }, + "sifive-e76": { + "before": 7796, + "after": 7806, + "saved": -10 + } + }, + "changed_after": true, + "sensitivity_rejected": 0 + }, + { + "size": 1000, + "seed": 1, + "chains": 3, + "input_sha256": "e09ac6c4a2ceff62b104e3a9ef2dc746fa410cc89d7a15152f6c8764f10edad1", + "before": { + "rocket-rv32": { + "before": 4374, + "after": 4399, + "saved": -25 + }, + "sifive-e76": { + "before": 7405, + "after": 7492, + "saved": -87 + } + }, + "after": { + "rocket-rv32": { + "before": 4374, + "after": 4374, + "saved": 0 + }, + "sifive-e76": { + "before": 7405, + "after": 7405, + "saved": 0 + } + }, + "changed_after": false, + "sensitivity_rejected": 1 + }, + { + "size": 1000, + "seed": 1, + "chains": 8, + "input_sha256": "b55cf1b7cf7028623cade7a5ee3fea037f56fe6a22f3bf7dfcbce6486e6ffcd5", + "before": { + "rocket-rv32": { + "before": 4132, + "after": 4178, + "saved": -46 + }, + "sifive-e76": { + "before": 6757, + "after": 6974, + "saved": -217 + } + }, + "after": { + "rocket-rv32": { + "before": 4132, + "after": 4098, + "saved": 34 + }, + "sifive-e76": { + "before": 6757, + "after": 6766, + "saved": -9 + } + }, + "changed_after": true, + "sensitivity_rejected": 0 + }, + { + "size": 1000, + "seed": 2, + "chains": 1, + "input_sha256": "625d8f5cf5e2a767c765a970cb8971b53910e3af8f34c217472463b9860cdef9", + "before": { + "rocket-rv32": { + "before": 4679, + "after": 4424, + "saved": 255 + }, + "sifive-e76": { + "before": 7885, + "after": 7558, + "saved": 327 + } + }, + "after": { + "rocket-rv32": { + "before": 4679, + "after": 4469, + "saved": 210 + }, + "sifive-e76": { + "before": 7885, + "after": 7677, + "saved": 208 + } + }, + "changed_after": true, + "sensitivity_rejected": 0 + }, + { + "size": 1000, + "seed": 2, + "chains": 2, + "input_sha256": "33b6292b00ba7d7b8cdea14c0ce913cf31abde60a6929b7256aa3a94666efafe", + "before": { + "rocket-rv32": { + "before": 4384, + "after": 4398, + "saved": -14 + }, + "sifive-e76": { + "before": 7481, + "after": 7436, + "saved": 45 + } + }, + "after": { + "rocket-rv32": { + "before": 4384, + "after": 4385, + "saved": -1 + }, + "sifive-e76": { + "before": 7481, + "after": 7484, + "saved": -3 + } + }, + "changed_after": true, + "sensitivity_rejected": 0 + }, + { + "size": 1000, + "seed": 2, + "chains": 3, + "input_sha256": "7f8a9c5ecaf3fb526178d785758ccdb72d3f02ff7ea047acd6f83122ba66bcfd", + "before": { + "rocket-rv32": { + "before": 4601, + "after": 4616, + "saved": -15 + }, + "sifive-e76": { + "before": 7836, + "after": 7928, + "saved": -92 + } + }, + "after": { + "rocket-rv32": { + "before": 4601, + "after": 4601, + "saved": 0 + }, + "sifive-e76": { + "before": 7836, + "after": 7836, + "saved": 0 + } + }, + "changed_after": false, + "sensitivity_rejected": 1 + }, + { + "size": 1000, + "seed": 2, + "chains": 8, + "input_sha256": "d709221396a4c82fc06d0864a34f52cd53db1e16ce61846154b6b755fec0ca17", + "before": { + "rocket-rv32": { + "before": 4736, + "after": 4774, + "saved": -38 + }, + "sifive-e76": { + "before": 7975, + "after": 8217, + "saved": -242 + } + }, + "after": { + "rocket-rv32": { + "before": 4736, + "after": 4736, + "saved": 0 + }, + "sifive-e76": { + "before": 7975, + "after": 7975, + "saved": 0 + } + }, + "changed_after": false, + "sensitivity_rejected": 0 + } + ], + "real_inputs": [ + { + "input": "benchmarks/cases/001_simple_add.dsl", + "allocator": "greedy", + "input_file_sha256": "baf76636ce5fd6ef5f8e2c878e12722b1563fbcf26724058355a618188f03888", + "input_sha256": "f3edd9eca9cdeb2ca9f7538f96adc28b7d26bf086856dcc3e73a56dd3624f16b", + "output_sha256": "f3edd9eca9cdeb2ca9f7538f96adc28b7d26bf086856dcc3e73a56dd3624f16b", + "changed": false, + "source_instructions_before": 3, + "source_instructions_after": 3 + }, + { + "input": "benchmarks/cases/002_simple_mul.dsl", + "allocator": "greedy", + "input_file_sha256": "2cc6fd1168b2fcb573fe07598c341fcccfa656eed1dfa01e858ea98df4a3d483", + "input_sha256": "fc28cb56dd98dfea0131ed692e9928791fc36f85df1bd8d4429fdc7128ad9f46", + "output_sha256": "fc28cb56dd98dfea0131ed692e9928791fc36f85df1bd8d4429fdc7128ad9f46", + "changed": false, + "source_instructions_before": 3, + "source_instructions_after": 3 + }, + { + "input": "benchmarks/cases/003_sub_div.dsl", + "allocator": "greedy", + "input_file_sha256": "9c7e86a097fecc457d23b4b2ed35f25b51a4940ba4dd3b9bb9ce67a6a71792eb", + "input_sha256": "1dc746e1f4782e2ff6d4100ee7c4383dda7360517f8caeb32e3c9600d6ee1f7f", + "output_sha256": "1dc746e1f4782e2ff6d4100ee7c4383dda7360517f8caeb32e3c9600d6ee1f7f", + "changed": false, + "source_instructions_before": 4, + "source_instructions_after": 4 + }, + { + "input": "benchmarks/cases/004_relu.dsl", + "allocator": "greedy", + "input_file_sha256": "ef5456f95c113f08549a47f3b5adc781e6cc6e570ee516bcc9f06fcdd10e4a73", + "input_sha256": "d3d8df84b346a7ab0d22bb1aab2d63a336a38a2060f2f89a2267e68dbc48f90f", + "output_sha256": "d3d8df84b346a7ab0d22bb1aab2d63a336a38a2060f2f89a2267e68dbc48f90f", + "changed": false, + "source_instructions_before": 3, + "source_instructions_after": 3 + }, + { + "input": "benchmarks/cases/005_gelu.dsl", + "allocator": "greedy", + "input_file_sha256": "542b23c4ae483e369c1c8d13b1dde0fa0d5ab6a0d29fe8a91655eee1a22c89e0", + "input_sha256": "ec409b1e73e9926f6bddac4653e65bcc4113ae38b7c45f57e1721a94fdc37cc9", + "output_sha256": "ec409b1e73e9926f6bddac4653e65bcc4113ae38b7c45f57e1721a94fdc37cc9", + "changed": false, + "source_instructions_before": 5, + "source_instructions_after": 5 + }, + { + "input": "benchmarks/cases/006_softmax.dsl", + "allocator": "greedy", + "input_file_sha256": "8b3893a44967aac5b7b41f5e35e3a195a08525d994e66d53be2510a71c26c2e4", + "input_sha256": "be601d717febb3fc5cc388006c1ddeb422e7798c111c991c90e55e9983a051b2", + "output_sha256": "be601d717febb3fc5cc388006c1ddeb422e7798c111c991c90e55e9983a051b2", + "changed": false, + "source_instructions_before": 3, + "source_instructions_after": 3 + }, + { + "input": "benchmarks/cases/007_matmul.dsl", + "allocator": "greedy", + "input_file_sha256": "2e4f2e1ef6160047ec2e768e232e85f3dfbb09bbd8be6a9e3bd05f607894de73", + "input_sha256": "6593f07c74593dd0dd5d3d0b7ead90ffb10999487e986f2ef2e2913fb772626c", + "output_sha256": "6593f07c74593dd0dd5d3d0b7ead90ffb10999487e986f2ef2e2913fb772626c", + "changed": false, + "source_instructions_before": 3, + "source_instructions_after": 3 + }, + { + "input": "benchmarks/cases/008_dot.dsl", + "allocator": "greedy", + "input_file_sha256": "72c2ca3a5352c1789fb23e1e87cb262156a0e14426d7f48ee2e00e66024c3abd", + "input_sha256": "3bff032c3176b922deebf5cc4d10752c26399d730900f70f86e399a01895d499", + "output_sha256": "3bff032c3176b922deebf5cc4d10752c26399d730900f70f86e399a01895d499", + "changed": false, + "source_instructions_before": 3, + "source_instructions_after": 3 + }, + { + "input": "benchmarks/cases/009_maxpool.dsl", + "allocator": "greedy", + "input_file_sha256": "3cd748f99d264804000cef9842541165d9a25be5187d39a510d68aaea32c8af5", + "input_sha256": "e9e33520b47f32c45109e08bd281bbfe28a7e3351bc324b046ff51a1fa655d1e", + "output_sha256": "e9e33520b47f32c45109e08bd281bbfe28a7e3351bc324b046ff51a1fa655d1e", + "changed": false, + "source_instructions_before": 7, + "source_instructions_after": 7 + }, + { + "input": "benchmarks/cases/010_exp_neg.dsl", + "allocator": "greedy", + "input_file_sha256": "9df34151036d4c3d73dc3ba6f8d17107255bdf701af92733e5247602f8162768", + "input_sha256": "68ecb134b1cbad37780766a2c266def44d7a164b95947bc79b60b62ce0086784", + "output_sha256": "68ecb134b1cbad37780766a2c266def44d7a164b95947bc79b60b62ce0086784", + "changed": false, + "source_instructions_before": 5, + "source_instructions_after": 5 + }, + { + "input": "benchmarks/cases/011_multi_op_chain.dsl", + "allocator": "greedy", + "input_file_sha256": "25db538d3f5ae57c07aa278183c25cf5d9e7e3956c500a46a8f256908fdc7450", + "input_sha256": "8d67be24550b4efe417f38be759f051eccbd6dd62da150bb9d3b3ddb25410d2e", + "output_sha256": "8d67be24550b4efe417f38be759f051eccbd6dd62da150bb9d3b3ddb25410d2e", + "changed": false, + "source_instructions_before": 5, + "source_instructions_after": 5 + }, + { + "input": "benchmarks/cases/012_nn_pipeline.dsl", + "allocator": "greedy", + "input_file_sha256": "1aaccab5e3046f7e4689e97de41e62180a95aab8b5a7db63f17a793522dfe314", + "input_sha256": "6119d69391edcb1f0c5cf5c76cb49d560a462a7e200122b1211a68c69c05eddd", + "output_sha256": "6119d69391edcb1f0c5cf5c76cb49d560a462a7e200122b1211a68c69c05eddd", + "changed": false, + "source_instructions_before": 5, + "source_instructions_after": 5 + }, + { + "input": "benchmarks/cases/013_for_sum.dsl", + "allocator": "greedy", + "input_file_sha256": "832090e59abad1dcfe8736f157987700a8bab1fd227a87557f2634bf5a620d17", + "input_sha256": "e6a67483bd2e19fff5961ed8e4dcba78f99a6526c8abc91159ba30b6169634c6", + "output_sha256": "e6a67483bd2e19fff5961ed8e4dcba78f99a6526c8abc91159ba30b6169634c6", + "changed": false, + "source_instructions_before": 7, + "source_instructions_after": 7 + }, + { + "input": "benchmarks/cases/014_for_dot.dsl", + "allocator": "greedy", + "input_file_sha256": "24233cfb94fccf87270b701fae399bc30c7e4f70977f2636081c83f20f14587b", + "input_sha256": "2794633f5fef2e489824537c1125cf7ad45842d68699bfdd555d70a80d56eeb2", + "output_sha256": "dc383bfea29d4b65fd2ca8cc3909e77700a0d30dc88628b8a6cea9637b46156b", + "changed": true, + "source_instructions_before": 8, + "source_instructions_after": 8 + }, + { + "input": "benchmarks/cases/015_for_relu.dsl", + "allocator": "greedy", + "input_file_sha256": "5801431f735f3ec4297252926161bb11c0b1c86f31f2b55f6c438c9188e8b959", + "input_sha256": "33d63cf40e55b7a23473216b48b4cf1b0780e198b8ecf50b3fa68056977efcd1", + "output_sha256": "33d63cf40e55b7a23473216b48b4cf1b0780e198b8ecf50b3fa68056977efcd1", + "changed": false, + "source_instructions_before": 8, + "source_instructions_after": 8 + }, + { + "input": "benchmarks/cases/016_if_simple.dsl", + "allocator": "greedy", + "input_file_sha256": "d3793ad34d44fbfa97bbc2c7efa67e5e70fea0ea66c3137854dfe1f7c14bcafa", + "input_sha256": "50f646337752da83a62f2fe48dac2a35b2b80f88254e6a0d0c5eb30cce417b0a", + "output_sha256": "50f646337752da83a62f2fe48dac2a35b2b80f88254e6a0d0c5eb30cce417b0a", + "changed": false, + "source_instructions_before": 8, + "source_instructions_after": 8 + }, + { + "input": "benchmarks/cases/017_while_sum.dsl", + "allocator": "greedy", + "input_file_sha256": "23572643c44ae61df2e91c6bb27922d1b554bb527add58cd16320703a8c2d34c", + "input_sha256": "c881ff0fdcb4b35daa7a55404d5dfa56938a87b08acbebc679fb13b22e7c2f55", + "output_sha256": "c881ff0fdcb4b35daa7a55404d5dfa56938a87b08acbebc679fb13b22e7c2f55", + "changed": false, + "source_instructions_before": 8, + "source_instructions_after": 8 + }, + { + "input": "benchmarks/cases/018_nested_if.dsl", + "allocator": "greedy", + "input_file_sha256": "40778de645a2eb5832691f0bb633c52995879aa06c1ad7e4094646a9d0d2496a", + "input_sha256": "2e9ae991e2c142808c5df35d168e4201a0f8629ccce595ce17a2d1f4757a6809", + "output_sha256": "2e9ae991e2c142808c5df35d168e4201a0f8629ccce595ce17a2d1f4757a6809", + "changed": false, + "source_instructions_before": 14, + "source_instructions_after": 14 + }, + { + "input": "benchmarks/cases/019_nested_loop.dsl", + "allocator": "greedy", + "input_file_sha256": "53674d90f875b441b992c1bb3eaf0756ab9b285feceb447bbc3fb213ad919675", + "input_sha256": "c30dd8118bdb25ccfe0130d77fb4292c6a6cdbfe1275735b8bfc2b186e194920", + "output_sha256": "3e858c5df8d96c99cd08b92bc4940c27c0cf4bbb68f9011d1fb53d552753a08f", + "changed": true, + "source_instructions_before": 12, + "source_instructions_after": 12 + }, + { + "input": "benchmarks/cases/020_constant_propagation.dsl", + "allocator": "greedy", + "input_file_sha256": "16a051d8803a4b6decdd993f0b68154fd6b24273a16ce20d632abf9961142212", + "input_sha256": "d81c17e38c008a146a0c310cb4c7160114d447a9ff7a3a0b844fd890c50986af", + "output_sha256": "d81c17e38c008a146a0c310cb4c7160114d447a9ff7a3a0b844fd890c50986af", + "changed": false, + "source_instructions_before": 7, + "source_instructions_after": 7 + }, + { + "input": "benchmarks/cases/021_dsl_if_else.dsl", + "allocator": "greedy", + "input_file_sha256": "1ea818cd4e37ac914e165002725a70cf07066ee32ce69b6d542d28100c9e3941", + "input_sha256": "974a838c712ec449abe533c7d50d1efd421ff13a55ba8764bf99f75c4c58695b", + "output_sha256": "974a838c712ec449abe533c7d50d1efd421ff13a55ba8764bf99f75c4c58695b", + "changed": false, + "source_instructions_before": 12, + "source_instructions_after": 12 + }, + { + "input": "benchmarks/cases/022_dsl_while_sum.dsl", + "allocator": "greedy", + "input_file_sha256": "a402859645bc2f4b3ed6bd28ba76926b86aa5d297c4b211609ff4d73d943fc3d", + "input_sha256": "62ca5693bcd26884ef6a812fc0a023760ca76dfab906b7b4dc8e851eba24eb8e", + "output_sha256": "62ca5693bcd26884ef6a812fc0a023760ca76dfab906b7b4dc8e851eba24eb8e", + "changed": false, + "source_instructions_before": 9, + "source_instructions_after": 9 + }, + { + "input": "benchmarks/cases/023_large_chain.dsl", + "allocator": "greedy", + "input_file_sha256": "30622aaf99808b1f9d2d822f6c01a8270f9a175092ce033523d691b35fd06338", + "input_sha256": "5ac4eaf076741169578137c348f3b86ff9108a03db2d0beac84f3572b269d812", + "output_sha256": "5ac4eaf076741169578137c348f3b86ff9108a03db2d0beac84f3572b269d812", + "changed": false, + "source_instructions_before": 10, + "source_instructions_after": 10 + }, + { + "input": "models/graph/cnn.onnx", + "allocator": "greedy", + "input_file_sha256": "169586fb90fc4a2fe1a45e3c27ac95e9c03cec58616b6dcc3b7ab7b4c24fc264", + "input_sha256": "ccd623643147ce5445275084f237fc5cd0575286f3c8ba0959f5af81b4eb5437", + "output_sha256": "b338bb0a55e5fe932284f3b86c1164c19aaa5f58659f3d54ab3bfd22d22b077b", + "changed": true, + "source_instructions_before": 59, + "source_instructions_after": 59 + }, + { + "input": "benchmarks/cases/001_simple_add.dsl", + "allocator": "linear", + "input_file_sha256": "baf76636ce5fd6ef5f8e2c878e12722b1563fbcf26724058355a618188f03888", + "input_sha256": "4d01252de2cdd80f1cbc979cf350bd266684caca500131ed47f52e8a3f06208a", + "output_sha256": "4d01252de2cdd80f1cbc979cf350bd266684caca500131ed47f52e8a3f06208a", + "changed": false, + "source_instructions_before": 3, + "source_instructions_after": 3 + }, + { + "input": "benchmarks/cases/002_simple_mul.dsl", + "allocator": "linear", + "input_file_sha256": "2cc6fd1168b2fcb573fe07598c341fcccfa656eed1dfa01e858ea98df4a3d483", + "input_sha256": "e7999951576e28979e1e2fea68a89bfcd6c4c656ecaf90d5a37613bb3a3af8b4", + "output_sha256": "e7999951576e28979e1e2fea68a89bfcd6c4c656ecaf90d5a37613bb3a3af8b4", + "changed": false, + "source_instructions_before": 3, + "source_instructions_after": 3 + }, + { + "input": "benchmarks/cases/003_sub_div.dsl", + "allocator": "linear", + "input_file_sha256": "9c7e86a097fecc457d23b4b2ed35f25b51a4940ba4dd3b9bb9ce67a6a71792eb", + "input_sha256": "d10638a758eb857c478e81c89dda59fd171f474cf5e4dc3a2a1f89efe15b8659", + "output_sha256": "d10638a758eb857c478e81c89dda59fd171f474cf5e4dc3a2a1f89efe15b8659", + "changed": false, + "source_instructions_before": 4, + "source_instructions_after": 4 + }, + { + "input": "benchmarks/cases/004_relu.dsl", + "allocator": "linear", + "input_file_sha256": "ef5456f95c113f08549a47f3b5adc781e6cc6e570ee516bcc9f06fcdd10e4a73", + "input_sha256": "becbd73f7669925d2586ba5af57141794f2e8a63e4a934c006e93581047123ff", + "output_sha256": "becbd73f7669925d2586ba5af57141794f2e8a63e4a934c006e93581047123ff", + "changed": false, + "source_instructions_before": 3, + "source_instructions_after": 3 + }, + { + "input": "benchmarks/cases/005_gelu.dsl", + "allocator": "linear", + "input_file_sha256": "542b23c4ae483e369c1c8d13b1dde0fa0d5ab6a0d29fe8a91655eee1a22c89e0", + "input_sha256": "9aea0ae56aee37c7784bbf44eff90e0a4ee2225218b61c46a4f2d297e4021005", + "output_sha256": "9aea0ae56aee37c7784bbf44eff90e0a4ee2225218b61c46a4f2d297e4021005", + "changed": false, + "source_instructions_before": 5, + "source_instructions_after": 5 + }, + { + "input": "benchmarks/cases/006_softmax.dsl", + "allocator": "linear", + "input_file_sha256": "8b3893a44967aac5b7b41f5e35e3a195a08525d994e66d53be2510a71c26c2e4", + "input_sha256": "4c4dcad4c37b1c0e145feee9dd5a8fd4f08aa4ecdd682976aa7de2d3d3ecf25a", + "output_sha256": "4c4dcad4c37b1c0e145feee9dd5a8fd4f08aa4ecdd682976aa7de2d3d3ecf25a", + "changed": false, + "source_instructions_before": 3, + "source_instructions_after": 3 + }, + { + "input": "benchmarks/cases/007_matmul.dsl", + "allocator": "linear", + "input_file_sha256": "2e4f2e1ef6160047ec2e768e232e85f3dfbb09bbd8be6a9e3bd05f607894de73", + "input_sha256": "7764fb91d82c3e2785c5b636a8b0d577fccefa9e4a3f9951dce9a6911a84ce19", + "output_sha256": "7764fb91d82c3e2785c5b636a8b0d577fccefa9e4a3f9951dce9a6911a84ce19", + "changed": false, + "source_instructions_before": 3, + "source_instructions_after": 3 + }, + { + "input": "benchmarks/cases/008_dot.dsl", + "allocator": "linear", + "input_file_sha256": "72c2ca3a5352c1789fb23e1e87cb262156a0e14426d7f48ee2e00e66024c3abd", + "input_sha256": "fc31488ab97240cf878dc69b073254e4bd6b31953b7624e6c12d6531f6fa3bff", + "output_sha256": "fc31488ab97240cf878dc69b073254e4bd6b31953b7624e6c12d6531f6fa3bff", + "changed": false, + "source_instructions_before": 3, + "source_instructions_after": 3 + }, + { + "input": "benchmarks/cases/009_maxpool.dsl", + "allocator": "linear", + "input_file_sha256": "3cd748f99d264804000cef9842541165d9a25be5187d39a510d68aaea32c8af5", + "input_sha256": "bd5262b29fc934d2683a2556c8a4ccbeefd60255c948a7a7bf0c368ab15bbed0", + "output_sha256": "bd5262b29fc934d2683a2556c8a4ccbeefd60255c948a7a7bf0c368ab15bbed0", + "changed": false, + "source_instructions_before": 7, + "source_instructions_after": 7 + }, + { + "input": "benchmarks/cases/010_exp_neg.dsl", + "allocator": "linear", + "input_file_sha256": "9df34151036d4c3d73dc3ba6f8d17107255bdf701af92733e5247602f8162768", + "input_sha256": "c049c02d126188a6e6b6f8cea700385d48a1fdc05869f024ccdf9db7918a6092", + "output_sha256": "c049c02d126188a6e6b6f8cea700385d48a1fdc05869f024ccdf9db7918a6092", + "changed": false, + "source_instructions_before": 5, + "source_instructions_after": 5 + }, + { + "input": "benchmarks/cases/011_multi_op_chain.dsl", + "allocator": "linear", + "input_file_sha256": "25db538d3f5ae57c07aa278183c25cf5d9e7e3956c500a46a8f256908fdc7450", + "input_sha256": "6fcaf203ae18edf26ae9075c899ce026d94cab2c6d070b36d69acf820dfcf881", + "output_sha256": "6fcaf203ae18edf26ae9075c899ce026d94cab2c6d070b36d69acf820dfcf881", + "changed": false, + "source_instructions_before": 5, + "source_instructions_after": 5 + }, + { + "input": "benchmarks/cases/012_nn_pipeline.dsl", + "allocator": "linear", + "input_file_sha256": "1aaccab5e3046f7e4689e97de41e62180a95aab8b5a7db63f17a793522dfe314", + "input_sha256": "f7435589899017de0e08d710d6a5d612625ee90ce58c265c326436ce493eef13", + "output_sha256": "f7435589899017de0e08d710d6a5d612625ee90ce58c265c326436ce493eef13", + "changed": false, + "source_instructions_before": 5, + "source_instructions_after": 5 + }, + { + "input": "benchmarks/cases/013_for_sum.dsl", + "allocator": "linear", + "input_file_sha256": "832090e59abad1dcfe8736f157987700a8bab1fd227a87557f2634bf5a620d17", + "input_sha256": "233af7938a03cbaf04c81b80cca399293e8d9c0a5cb323eff72b868978597053", + "output_sha256": "233af7938a03cbaf04c81b80cca399293e8d9c0a5cb323eff72b868978597053", + "changed": false, + "source_instructions_before": 7, + "source_instructions_after": 7 + }, + { + "input": "benchmarks/cases/014_for_dot.dsl", + "allocator": "linear", + "input_file_sha256": "24233cfb94fccf87270b701fae399bc30c7e4f70977f2636081c83f20f14587b", + "input_sha256": "9b2f2c641c59ffa3bec405c8192ccf1a1bd20710ccd06947bf9c8ae8869c2e37", + "output_sha256": "5536bc2741af82af5a097a6c86e34f83421c83a6b4369f55ad4e88e21b4bd336", + "changed": true, + "source_instructions_before": 8, + "source_instructions_after": 8 + }, + { + "input": "benchmarks/cases/015_for_relu.dsl", + "allocator": "linear", + "input_file_sha256": "5801431f735f3ec4297252926161bb11c0b1c86f31f2b55f6c438c9188e8b959", + "input_sha256": "fe3977c07755aa44e646d9519fbb847318d95fbfb586ead562b7c4dd3c5ef5ae", + "output_sha256": "fe3977c07755aa44e646d9519fbb847318d95fbfb586ead562b7c4dd3c5ef5ae", + "changed": false, + "source_instructions_before": 8, + "source_instructions_after": 8 + }, + { + "input": "benchmarks/cases/016_if_simple.dsl", + "allocator": "linear", + "input_file_sha256": "d3793ad34d44fbfa97bbc2c7efa67e5e70fea0ea66c3137854dfe1f7c14bcafa", + "input_sha256": "e0b38a74986605038cd7870c84706c5a5180e4f65208c8e3886d4aeff70b3df9", + "output_sha256": "e0b38a74986605038cd7870c84706c5a5180e4f65208c8e3886d4aeff70b3df9", + "changed": false, + "source_instructions_before": 8, + "source_instructions_after": 8 + }, + { + "input": "benchmarks/cases/017_while_sum.dsl", + "allocator": "linear", + "input_file_sha256": "23572643c44ae61df2e91c6bb27922d1b554bb527add58cd16320703a8c2d34c", + "input_sha256": "87043acd64c8870d305b35e6ed7441dd5e3bb5edd6938dee35adf96691da6901", + "output_sha256": "87043acd64c8870d305b35e6ed7441dd5e3bb5edd6938dee35adf96691da6901", + "changed": false, + "source_instructions_before": 8, + "source_instructions_after": 8 + }, + { + "input": "benchmarks/cases/018_nested_if.dsl", + "allocator": "linear", + "input_file_sha256": "40778de645a2eb5832691f0bb633c52995879aa06c1ad7e4094646a9d0d2496a", + "input_sha256": "a4fda91742e159d5a5514bc188e9c17083739fdbda0eca5e9cea8d93b31e6922", + "output_sha256": "a4fda91742e159d5a5514bc188e9c17083739fdbda0eca5e9cea8d93b31e6922", + "changed": false, + "source_instructions_before": 14, + "source_instructions_after": 14 + }, + { + "input": "benchmarks/cases/019_nested_loop.dsl", + "allocator": "linear", + "input_file_sha256": "53674d90f875b441b992c1bb3eaf0756ab9b285feceb447bbc3fb213ad919675", + "input_sha256": "4d4288f5dd42748b28b2d666a368b8a7760718ec7461d4ecac41a9b550a6031d", + "output_sha256": "3de9e1d23fc3dc9ab2a64fe9c358be2bc500b204163e97fb61d020eb15d5f8d3", + "changed": true, + "source_instructions_before": 12, + "source_instructions_after": 12 + }, + { + "input": "benchmarks/cases/020_constant_propagation.dsl", + "allocator": "linear", + "input_file_sha256": "16a051d8803a4b6decdd993f0b68154fd6b24273a16ce20d632abf9961142212", + "input_sha256": "068e1418be5d383d8104631f7eba2555ae68347c87b98f32cb9bb7e514c9a9bb", + "output_sha256": "068e1418be5d383d8104631f7eba2555ae68347c87b98f32cb9bb7e514c9a9bb", + "changed": false, + "source_instructions_before": 7, + "source_instructions_after": 7 + }, + { + "input": "benchmarks/cases/021_dsl_if_else.dsl", + "allocator": "linear", + "input_file_sha256": "1ea818cd4e37ac914e165002725a70cf07066ee32ce69b6d542d28100c9e3941", + "input_sha256": "d373391907bf945f50ca382e0433a1fa1fc2cf1f0483586a93786b9f232dc603", + "output_sha256": "d373391907bf945f50ca382e0433a1fa1fc2cf1f0483586a93786b9f232dc603", + "changed": false, + "source_instructions_before": 12, + "source_instructions_after": 12 + }, + { + "input": "benchmarks/cases/022_dsl_while_sum.dsl", + "allocator": "linear", + "input_file_sha256": "a402859645bc2f4b3ed6bd28ba76926b86aa5d297c4b211609ff4d73d943fc3d", + "input_sha256": "32845f2b0eebdeaa5bbc1e6b686d2142897a637f910fd5dcdf7906b52b95af57", + "output_sha256": "32845f2b0eebdeaa5bbc1e6b686d2142897a637f910fd5dcdf7906b52b95af57", + "changed": false, + "source_instructions_before": 9, + "source_instructions_after": 9 + }, + { + "input": "benchmarks/cases/023_large_chain.dsl", + "allocator": "linear", + "input_file_sha256": "30622aaf99808b1f9d2d822f6c01a8270f9a175092ce033523d691b35fd06338", + "input_sha256": "ef45f2b393db967cc911233053dc7963427d80c0a1af2fb015ed907540ee7c09", + "output_sha256": "ef45f2b393db967cc911233053dc7963427d80c0a1af2fb015ed907540ee7c09", + "changed": false, + "source_instructions_before": 10, + "source_instructions_after": 10 + }, + { + "input": "models/graph/cnn.onnx", + "allocator": "linear", + "input_file_sha256": "169586fb90fc4a2fe1a45e3c27ac95e9c03cec58616b6dcc3b7ab7b4c24fc264", + "input_sha256": "8aede08c2802ec0ec0ae90bcd62565f8f690a5b377aec5ba3f6daa06108e7cff", + "output_sha256": "532b4432ffaf214aa972e576ed66b15fd482f5cf1cebde3734c29221ba8610b5", + "changed": true, + "source_instructions_before": 47, + "source_instructions_after": 47 + } + ] +} diff --git a/docs/topic-18/topic18-review.md b/docs/topic-18/topic18-review.md new file mode 100644 index 0000000..d667007 --- /dev/null +++ b/docs/topic-18/topic18-review.md @@ -0,0 +1,381 @@ +# 分支评审报告 — pr-59「指令调度器启用与安全修复」(Mastttttter)× 对照 topic_18 + +> 评审日期:2026-09-14 第一轮;2026-09-15 第二轮修订 +> 评审对象:`pr-59`(第一轮 HEAD `c51200e`;第二轮 HEAD `035d180`,含新增 `dcf5f09`「feat(benchmark): add instruction scheduling A/B reports」与 upstream/main 合流;特性提交 `2c71bd8`「feat(backend): implement safe local instruction scheduling」,2026-09-11;文档提交 `ad9c806`「upd: rename doc」,2026-09-14;作者 Mastttttter) +> 对照分支:`topic_18`(`742a561`,2026-09-13,作者 Seven Gao,「rewrite instruction scheduler with intra-block list scheduling」) +> 共同基线:`main` `73c3926`(第一轮);第二轮合流点 `main` `99538fe`;改动规模:pr-59 vs 第一轮 main = 16 files, +3180/−690(其中特性提交 12 files, +1670/−690);topic_18 vs merge-base = 3 files, +1508/−559;第二轮新增基准提交 6 files, +731/−5 +> 评审人:opencode(审查角色:架构师 + 工程师双视角) +> **第二轮修订说明**:本轮把验证目标从「调度是否正确」扩展为「调度是否真的有效」。新增 §4.5(llvm-mca 独立微架构验证、全量真实负载覆盖审计、独立解释器差分执行、发射形式静态对照、新增 Benchmark 文档逐项复现、topic_18 断言复核);相应更新 §0、§5(新增 F11–F16)、§7、§8、§9,并在 §10 给出「值得合入、但需先修 4 项 P1」的最终结论。第一轮正文保留。 +> 本轮证据产物:`/tmp/opencode/t18/{mca,coverage,semantics,topic18}/`,全部命令与原始 JSON 可复现。 + +--- + +## 0. 结论速览 + +| 对象 | 结论 | +|------|------| +| **pr-59(主评审对象,正确性与工程)** | **有条件合入**。未发现正确性阻断级缺陷;安全设计(语义精确分类 → 区域切分 → DAG → 独立验证器 → 收益门控 → 严格模式 → 不写部分输出)是本仓库同类 pass 中最完整的一次。第二轮独立验证:140 对差分执行 0 语义失败、全量 848 测试通过。合入前必须处理 4 项 P1:删除误提交文件 `:memory:.ses`(F1)、同步 topic-18 文档状态(F2)、修正 div/rem 成本模型(F11)、修 linear 分支目标发射(F12)。 | +| **pr-59(有效性,第二轮新增)** | **仅模型内有效,真实收益远低于声称且可能为负**。llvm-mca 在合成语料上只兑现模型声称的 **8.1%**(rocket-rv32;40/72 region 反而变慢),sifive-e76 整体 **-236 拍**;真实负载 24 个输入只有 3 个产生换序,动态指令数 0 变化。根因:div 模型非阻塞(16 拍)与真实(rocket 33 拍且阻塞发射、e76 66 拍)不符,mul 3 拍偏低。`saved_cycles` 只能当方向上界,不能当硬件收益。 | +| **topic_18(对照)** | **不建议作为合入基线**。成本模型存在实质偏差(非流水除法阻塞整条流水、周期数不计末条指令延迟),并且不识别本仓库后端的 `base(offset)` 存储语法,在真实 `cnn.onnx` 产物上 12/59 条指令被迫钉死;验证器与集成强度也弱于 pr-59。第二轮复核 T1/T2/T3/T6 全部成立(单条 mul 报 1 拍、`div+独立 add` 17 拍/15 stall、12 条 spill store 钉死、benchmark 用非物理寄存器且 3 链 0% 改善)。其操作数宽容策略与 `.label` 处理可作为参考。 | +| **Benchmark 文档(`dcf5f09`,第二轮新增)** | **可信**。`docs/topic-18/18-指令调度器Benchmark.md` 的固定用例、CNN 静态 A/B(`not_modeled`)、合成规模、CI 集成的全部数字与声称逐项复现一致;口径明确不声称硬件加速、不把 CNN 解释为加速、不用 TinyFive 证明周期下降,属于少见的诚实报告。仅 `.venv/bin/python` 与 standalone 生成命令两处本地运行说明有误(F15)。 | +| **共性结论** | 两者都是「文本层 V1」,均未兑现 topic-18 设计文档要求的 post-RA 结构化调度;`--schedule` 均保持默认关闭,主收益为局部静态模型估算(非硬件周期),不减少动态指令数。**本 topic 对「动态指令数 vs LLVM 4.2x」这一项目核心指标贡献为 0,不应计入性能达标项。** | + +--- + +## 1. 特性概述 + +### 1.1 "enable" 与 "fix" 的具体含义 + +pr-59 的改动可以拆成两件事: + +**(a)enable:把调度器真正接进生产编译路径并加装控制面** + +- `scratchv/compiler.py:490-524`:`_run_asm_passes` 改为调用 `schedule_assembly(asm_text, ScheduleConfig(strict=...))`,把 `stats["schedule"]`(含逐区域明细与 diagnostics)写进 `CompileResult.stats`,warning 级诊断进入 `CompileResult.warnings`;`scratchv/compiler.py:244-247` 增加配置校验(`--schedule` 仅限 riscv 后端;`--schedule-strict/--schedule-report` 必须伴随 `--schedule`);`scratchv/compiler.py:336-340` 捕获 `ScheduleError` 并以编译失败返回,**不写部分输出**。 +- `scratchv/main.py:95-99`:新增 `--schedule-strict`(验证失败即失败)与 `--schedule-report`(打印模型估算报告)。 +- 旧生产接线(`main`)是 `parse_instructions → build_dag → schedule → "\n".join(f" {op} " + ...)` 的文本重排,会**破坏汇编**。在 `main` 实测 `--schedule`(`benchmarks/cases/017_while_sum.dsl`)产出:`.text/.globl` 变 `text/globl`、`main:`/`.entry:` 等标签全部丢失、`ret` 行丢失换行、分支顺序错乱——不可汇编。pr-59 用「保留原始行 + 按槽位换行」的原地重写替代了它(`inst_scheduler.py:437-439`)。 + +**(b)fix:按实现计划重写调度核心(`docs/topic-18/18-指令调度器实现计划.md`)** + +- 新增 3 个模块,语义/模型/验证与算法分离:`schedule_semantics.py`(一型一解析、未知形式一律 barrier)、`schedule_model.py`(单发射资源占用模型 + 顺序敏感估算)、`schedule_verify.py`(独立于建图的验证器)。 +- 依赖图从「仅 RAW/WAW、无内存/控制边界」(设计文档 §16.2 缺陷 4)补齐为 RAW/WAR/WAW + memory 全保序 + FP flags + control/terminator 固定(`inst_scheduler.py:68-106`)。 +- 优先级计算修正(反向拓扑 + 边距离,`inst_scheduler.py:114-120`),测试给出精确高度 `[6,4,1]`。 +- `machine_instrs_from_scheduled` 不再把未知 opcode 回退为 `MV`,改为显式抛错(`inst_scheduler.py:462-493`,修复设计 §16.2 缺陷 8)。 +- 收益判定从「与顺序无关的延迟求和」改为共享模型的顺序敏感估算(`schedule_model.py:78-101`),并要求**严格更低**才应用(`inst_scheduler.py:416-419`)。 +- 重写 `benchmarks/bench_inst_scheduler.py`:改用合法物理寄存器、走生产入口 `schedule_assembly`、明确声明「本地静态模型估算,非硬件周期」。 + +### 1.2 模块清单与公共接口 + +| 文件 | 行数 | 职责 | 关键接口 | +|------|------|------|----------| +| `scratchv/backend/schedule_semantics.py` | 294 | 指令语义:`describe()` 精确匹配 30+ 种 RISC-V 形式;`SchedInst` 不可变(frozen),def/use 由 opcode 重算而非调用方提示 | `describe`、`SchedInst`、`ScheduleError` | +| `scratchv/backend/schedule_model.py` | 101 | 单发射教学模型:ALU/load/mul/div/FP 延迟与资源占用;顺序敏感 `estimate_order` | `ScheduleModel`、`estimate_order` | +| `scratchv/backend/schedule_verify.py` | 51 | 独立验证:身份/置换/边界/固定指令/读取来源与最终写值/访存·FP 副作用顺序/DAG 边序 | `verify_schedule` | +| `scratchv/backend/inst_scheduler.py` | 534 | 区域切分、DAG、列表调度、收益门控、原地重写、统计与诊断、CLI | `InstructionScheduler`、`parse_instructions`、`ScheduleConfig`、`ScheduleResult`、`schedule_assembly` | + +集成面:`scratchv/backend/__init__.py` 导出 3 个新符号;`compiler.py` / `main.py` 见 §1.1;测试新增 `tests/test_inst_scheduler_integration.py`(279 行)与 `tests/test_inst_scheduler_safety.py`(314 行),并重写 `tests/test_inst_scheduler.py`。 + +### 1.3 topic_18 对照实现概览 + +单文件 1065 行自包含实现:`OpcodeInfo` 表 → `classify_operands` → `split_blocks`(含 `.label` 伪标签)→ RAW/WAR/WAW + 保守内存 chain 构建 → 列表调度 → 顺序敏感 scoreboard 估算 → `verify`(置换 + 边序 + 末条 pinned)→ `schedule_asm` 原地重建。集成方式为 `compiler.py` 的 `_asm_pass_stats` 属性 + `ScheduleConfig()`(strict 未接线),保留 legacy API。 + +--- + +## 2. 评审材料与方法 + +### 2.1 阅读清单 + +- pr-59:`inst_scheduler.py`、`schedule_semantics.py`、`schedule_model.py`、`schedule_verify.py`、`compiler.py`(diff 与现场代码)、`main.py`、`benchmarks/bench_inst_scheduler.py`、3 个测试文件、`docs/topic-18/` 4 份文档、`.github/workflows/ci.yml`、`git diff main...pr-59` 全量。 +- topic_18:worktree `/tmp/opencode/topic18-wt`,`inst_scheduler.py` 1065 行、`compiler.py` diff、`tests/test_inst_scheduler.py` 617 行、原 benchmark。 +- 共同依赖:`scratchv/backend/_asm_parser.py`、`riscv_encoder.py`(确认 `max`/大立即数 `li`/分支立即数为多指令展开伪操作;确认 `base(offset)` 是后端合法语法)。 + +### 2.2 执行的命令与结果 + +| 命令 | 结果 | +|------|------| +| `python3.11 -m pytest tests/test_inst_scheduler*.py -q`(pr-59) | **105 passed, 19 skipped**(skip 全部为 `clang` + `qemu-riscv32` 缺失) | +| `python3.11 -m pytest tests/ -q`(pr-59) | **760 passed, 19 skipped**(全量无回归) | +| topic_18 worktree:`pytest tests/test_inst_scheduler.py -q` / `pytest tests/ -q` | **45 passed** / **594 passed** | +| `python3.11 -m benchmarks.bench_inst_scheduler --repeats 5`(pr-59) | 10→1000 条:saved 5→430 cycles(约 14%→19% 模型节省);5000 条区域超限整体跳过 | +| topic_18 自带 benchmark | 3 依赖链全部 **0% 改善**;1 链 9.5%;5000 条 2.4s | +| 对照语料(同一批 400 个随机直线块,逐字节同一 seed) | 变更率:pr-59 **332/400**(含 div/rem)、**95/400**(无 div/rem);topic_18 **16/400**、**0/400** | +| 独立 RV32I 解释器对拍(`/tmp/opencode/sem_check.py`,整数 + 访存 + 符号,含 div/rem/内存读写) | pr-59 **400/400 语义一致**(含 332 次真实换序);topic_18 **400/400 语义一致**(16 次换序);两分支均无失败 | +| 随机混合输入 fuzz(3000 例,含宏/条件汇编/CRLF/畸形操作数/多 section) | pr-59 无异常泄漏、无行丢失、strict 仅抛 `ScheduleError` | +| 真实负载 `models/graph/cnn.onnx --schedule` | 基线产物两分支逐字节相同;两分支调度后产物**也相同**(3 处 `li`/`mv` 互换);pr-59 建模 49/59、模型 59→55 cycles、moved 6;topic_18 将 12 条 `sw sp(...)` 钉死并刷 12 条 warning | +| `main` 对照:`--schedule` 产出 | 标签/指示符丢失、顺序错乱、不可汇编(见 §1.1,证明"修复"对象真实存在) | +| `git diff --shortstat main...pr-59` | 16 files changed, +3180/−690 | + +环境说明:仓库 `.venv` 为 Python 3.8(与本项目 3.12 目标不符,`tuple[str, ...]` 直接报错),本次统一用 `/usr/local/bin/python3.11`;本机无 `clang`/`qemu-riscv32`,因此两个分支的「真实汇编 + 执行对拍」用例全部 skip(见 F10)。CI(`.github/workflows/ci.yml`)同样未安装这两个工具。 + +### 2.3 独立验证脚本(可复现) + +- `/tmp/opencode/diff_bench.py`:同一生成器驱动两个实现的对照基准; +- `/tmp/opencode/sem_check.py`:自写 RV32I 子集解释器(add/sub/mul/div/rem/逻辑/移位/比较/li/lui/mv/LW/SW/分支/标签),对调度前后产物做终态寄存器 + 内存逐项比对; +- `/tmp/opencode/one_case.py`、`one_case18.py`:单例分歧定位; +- worktree:`/tmp/opencode/topic18-wt`、`/tmp/opencode/main-wt`(评审结束已清理,ScratchV 工作区保持干净)。 + +--- + +## 3. 架构师视角评审 + +### 3.1 分层与模块边界(优) + +pr-59 把原单文件、985 行、自带解析环(`parse_instructions → build_dag → schedule → 文本重拼`)的调度器,重构成单向依赖的四层: + +``` +schedule_semantics (指令语义,唯一解析点) + ↑ ↑ +schedule_model (时序/资源) schedule_verify (独立校验) + ↑ +inst_scheduler (区域/DAG/列表调度/重写/统计) +``` + +- 语义对象不可变(`frozen=True`),def/use **必须**从 opcode 重算(`schedule_semantics.py:276-282`),从根上消灭"调用方提示覆盖真实语义"与"按操作数位置猜 def-use"两类旧缺陷;未知形式返回 `barrier_reason` 而不是猜。 +- `schedule_verify` 的检查项独立于建图:除 DAG 边序外,还独立比较**每次寄存器读取来源**(`(id, reg) → 生产指令 id`)与**最终写值归属**、访存与 FP 副作用序列、pinned/terminator 不动(`schedule_verify.py:35-51`)。即使建图漏了某条依赖,读来源比较仍能拦截——这是比"用同一张图验证同一张图"明显更强的闭环。 +- 这是 V1 文本入口下能做到的合理边界;未来若按设计文档迁到 post-RA 结构化 `MachineInstr`,`schedule_model`/`schedule_verify`/调度算法可原样复用,只需替换 `SchedInst` 适配层。**可演进性良好**。 + +### 3.2 与 topic-18 设计文档的架构对齐度(偏) + +设计文档(2026-07-25,Proposed v0.1)要求:post-RA/pre-emission、结构化 `MachineInstr`、输入不解析汇编、验证失败 fail-stop、拒绝 `--reg-alloc linear --schedule`。pr-59 实现的是**计划文档**的收缩版 V1:文本入口、默认 `linear` 也可调度、失败默认局部回退 + strict 才失败。两者是显式记录的范围调整(计划 §1 自认"调整原设计"),Review 文档也支持安全降级。架构上可接受,**但代价是设计文档的验收门槛全部未达成**,且 4 份文档互相冲突(见 F2)。结论:V1 选择务实、方向无误;需要把"这一版是计划 V1、设计仍是 V2 backlog"写清楚,避免后续以设计文档评估本实现时产生误判。 + +### 3.3 安全体系(优,本仓库同类最强) + +pr-59 在五个层次上做了防御,且每层都有测试锚点: + +1. **解析层**:`describe()` 只接受精确支持的形式(寄存器别名校验、立即数范围、分支目标必须符号/数字标签、store 两种语法);`li` 仅小立即数、`jal/jalr/call/auipc/la/lr/sc/amo/csr` 等一律 barrier —— 正确识别了后端多指令展开伪操作(`max` 展开为 6 行、大 `li` 展开为 LUI+ADDI、分支立即数展开为 2 条),从不假设"一行=一条指令"。 +2. **布局层**:`_unsafe_layout` 对宏/条件汇编/行内分号/续行/当前地址表达式/数字控制流目标做**整文件熔断**(`inst_scheduler.py:287-335`),宁可零收益也不冒险。 +3. **区域层**:标签、指示符、barrier、terminator 全部切区,跨区调度在 `build_dag` 直接拒绝("Multiple regions")。 +4. **验证层**:候选顺序先过 `verify_schedule` 才允许落盘;收益不严格改善则保留原序;失败恢复整区(strict 则整个编译失败且不写输出)。 +5. **集成层**:后端/参数组合先校验;`CompileResult.stats/warnings` 可观测;失败路径不产生部分文件(集成测试 `test_stats_are_per_compilation_and_strict_failure_leaves_output_intact` 断言旧输出文件未被覆盖)。 + +### 3.4 可观测性与可演进性(良) + +- `ScheduleResult.stats` 提供 `input/modeled/moved/applied/skipped/original_cycles/final_cycles/saved_cycles/regions[]`(含逐区依赖计数与 issue cycles),`--schedule-report` 打印人类可读摘要,`diagnostics` 带行号与原因;模型名、override、估算口径("以就绪输入为前提的局部静态估算")都写进了 stats,避免把模型数当实测吹。 +- 模型可注入 override(`ScheduleModel(overrides=...)`),测试用它验证模型共享与拷贝语义。 +- 遗留 API(`InstructionScheduler.estimate_cycles/report`、`machine_instrs_from_scheduled`)保留但语义收紧为 fail-loud,兼容旧测试的同时移除静默回退。 + +### 3.5 架构层面问题 + +- **覆盖率与可观测性错配(F3)**:真实产物中 `max`、`slt rd,0,rs`、`mv rd,0`、`slt rd,rd,1` 都是后端合法形式(`_reg_num` 把未知/字面量静默映射为 x0),却被判 barrier;`cnn.onnx` 上 10/59 未建模。这些拒绝只是 `info` 级诊断,默认 CLI 完全不显示,用户会误以为"整份文件都调度过了"。 +- **整文件熔断粒度过粗(F5)**:任意一处 `.macro`/`;`/续行导致全文件零调度,`skipped_regions=1` 且诊断固定指向第 1 行。 +- **模型是教学模型(F6)**:单发射、资源占用、无 cache/分支预测/转发细节;报告已明确免责,但在硬件校准前不应默认开启(当前 `--schedule` 默认关闭,符合设计)。 + +--- + +## 4. 工程师视角评审 + +### 4.1 代码质量(良~优) + +- 类型注解完整,docstring 说明设计意图与 LLVM 参考位置(`ScheduleDAGInstrs::addPhysRegDeps` 等);命名达意(`barrier_reason`、`occupancy`、`edge_kinds`)。 +- 列表调度实现(`inst_scheduler.py:122-177`)用「时间有序 pending 堆 + 每资源优先级堆」,`min(choices)` 三元组 `(-priority, ready_time, index)` 唯一且确定;除法按 `occupancy=latency` 占用资源但不阻塞其它资源——比 topic_18 的"整条 EX 停 16 拍"更贴近真实单发射多流水线结构,且 `div+ALU` 场景模型为 16 拍(topic_18 为 17 拍)。 +- 原地重写用 `output[slot.id] = inst.raw_line + lines[slot.id].ending` 把换行按槽位保留(含 CRLF、末行无换行),三个参数化用例覆盖 `\n`/`\r\n`/末行无换行。 +- 收益门控、幂等性(重跑输出不变)、确定性(同输入两次调度逐字节一致)都有断言。 +- 小瑕疵:`_unsafe_layout` 的 `.` 正则对字符串常量里的 `;`/`\` 会误报(安全方向);`InstructionScheduler(latency_model={}, model=m)` 因空 dict 非 None 而触发互斥 `ValueError`;`_run_asm_passes` 内重复的后端校验是死分支;`InstructionScheduler.report` 与 `ScheduleResult.report` 两份报告 API 并存。 + +### 4.2 动态验证结果 + +- **修复对象真实存在**:`main` 的 `--schedule` 输出直接丢标签、丢点、乱序(§1.1),pr-59 同输入产出与无调度基线逐字节一致(该样例无可调度收益区域)。 +- **随机代码语义对拍**:自写解释器在 400 个含 `div/rem/内存读写/WAW/WAR` 的随机块上,pr-59 换序 332 次、0 语义失败;topic_18 换序 16 次、0 语义失败。pr-59 的换序更激进但都被自身验证器 + 解释器双向确认。 +- **真实 CNN 产物**:`--schedule` 与基线对比只做了 3 处 `li`/`mv` 互换(模型 59→55 拍,moved 6),产物仍可被仓库编码器接收;更激进收益受限于后端生成的块很小(每块 2~4 条,标签/terminator 密集)以及 `sw sp(...)`/`max` 等边界。 +- **故障注入**:裁行/倒序/自环三种注入分别验收「整区恢复 + warning」与「strict 编译失败 + 旧输出不被覆盖」,与设计 §8 一致。 + +### 4.3 测试质量 + +- 优点:精确断言为主(边类型 `{"WAR"}`、`{"RAW","WAW"}`、优先级 `[6,4,1]`、issue cycles `[0,2,3,4]→[0,1,2,3]`、stall 1→0),不搞"长度>0"式弱断言;含失败注入、CRLF 保真、unsafe 矩阵、fuzz-style 确定性、配置组合矩阵(3 分配器 × DAG/非 DAG)、`--schedule` 关闭时**禁止**调用调度器的 monkeypatch 哨兵。 +- 缺口:没有仓库内的随机/性质测试(随机只测确定性,不测语义;语义测试依赖 clang/qemu,天天 skip);没有 qemu/clang 的 CI job;未对 `ScheduleModel` 的延迟表做数值回归;未对 benchmark 收益设阈值断言。 +- 全量 760 通过说明未伤及既有全部用例。 + +### 4.4 工程卫生与合入机制 + +- **F1(必修)**:`ad9c806` 误提交根目录文件 `:memory:.ses`(51 字节,内容为时间戳 + UUID,明显是会话/工具残留),同时把 `2c71bd8` 刚加进 `.gitignore` 的 `/docs/mytopic/` 又删掉了。应删除该文件并把它加入 `.gitignore`。 +- **F2(必修)**:`docs/topic-18/` 4 份文档是"实现前快照",且互相矛盾:设计要求"输入不解析汇编、fail-stop、拒绝 `linear+schedule`";计划/说明坚持文本入口、局部回退、默认 linear,并写明"尚未修改实现代码"——而同一分支的代码已经实现。文档里引用的 `18-指令调度器真实状态调研.md`、`18-指令调度器SPEC.md`、`18-指令调度器-详细设计-joska.md` 在仓库中不存在。合入前应至少加版本/状态标注与"与实现差异"章节。 +- 分支历史干净,特性提交与文档提交分离;第二轮合入 upstream/main 并新增基准提交 `dcf5f09`;无本仓库其他 topic 的改动夹带。 + +### 4.5 第二轮独立验证:有效性与覆盖度(2026-09-15 新增) + +第一轮证明了「调度改得对」,第二轮回答「调度到底有没有用、在哪里没用」。方法全部独立于实现方的教学模型:LLVM 官方微架构模型 llvm-mca、自写 RV32IM 解释器差分执行、全量真实负载编译审计、发射端/识别端静态对照、以及新增 Benchmark 文档逐项复现。产物见 `/tmp/opencode/t18/`。 + +#### 4.5.1 llvm-mca 独立微架构验证(LLVM 18.1.8,`rocket-rv32` / `sifive-e76`) + +- 合成语料(`benchmarks.bench_inst_scheduler._gen_instructions`,sizes 10–1000 × seeds 42/1/2 × dep_chains 1/2/3/8,共 72 个 region): + - ScratchV 教学模型声称节省 9642 拍;llvm-mca `rocket-rv32` 实测仅节省 **783 拍(8.1% 兑现)**,win/tie/loss = **25/7/40**;`sifive-e76` 整体 **-236 拍(净负优化)**。 + - 按依赖链分解:`dep_chains=1` → 18/0/0(+1314,兑现 49%);`=2` → 7/1/10(-58);`=3` → 0/2/16(-195);`=8` → 0/4/14(-278)。**只有单依赖链形态在两个 CPU 上一致为正。** + - 最小负例(size=100, seed=42, dep_chains=3):模型 240→187,llvm-mca 449→462(**调度后慢 13 拍**)。逐 region 复核确认该 region 内 div 被大幅提前。 + - 根因是模型参数:`schedule_model.py` 中 `div/rem` latency=16 且"只占除法器、不阻塞其他发射";真实 `rocket` div latency=33 且阻塞发射,`sifive-e76` div=66;`mul` 模型 3 拍,rocket 实测 4 拍。把 div 提前在模型里赚钱、在真实流水线里亏钱。 +- 真实产物:只有 `cnn.onnx` 产生 4 个 applied region(均为 3–4 条,把独立 `li`/`mv` 塞进 `mul` 延迟槽),rocket/e76 分别确认 +4/+3 拍;23 个 DSL 用例在两种分配器下几乎全部 `no_improvement`。 +- 口径说明:llvm-mca 也是静态模型(无 cache/分支预测、region 局部输入假设),但使用真实 CPU 参数且由 LLVM 维护;结论在 rocket 与 e76 两套不同参数上互相验证,比教学模型可信。 + +#### 4.5.2 真实负载覆盖审计(CompilerDriver 全量编译 24 个输入) + +- CLI 默认(greedy):211 条指令、建模 180(85.3%)、**有增益的文件仅 3/24**(`014_for_dot` 模型 +1、`019_nested_loop` +1、`cnn.onnx` +4),共移动 10 条指令;库默认(linear):覆盖率 52.3%、仅 cnn 有 applied。 +- 可调度 region 平均 **1.86 条**(26 个 region 只有 1 条,最大 5 条),官方合成块平均约 310 条——相差约 167 倍。真实代码被标签、terminator、barrier 切成碎片,调度器没有施展空间。 +- 未建模指令 Top:`j`×28、`max`×13、`bnez`×12、`slt`(字面量操作数)×6、`bge`×5 等;`j`/`bnez`/`bge` 大量缺失的直接原因是上游 linear 分配器不输出分支目标(见 4.5.4)。 +- 指令数不变量:24/24 文件调度前后 199→199,逐 opcode 完全一致——**调度不改变动态指令数**,与 §0 的共性结论一致。 + +#### 4.5.3 独立解释器语义对拍(正确性复核) + +- 自写 RV32IM 解释器对随机块与真实产物做调度前后差分执行:**140 对拍、执行 20022 条、mismatch 0**;覆盖 lw-use、两条 div 竞争、WAR/WAW、同址 store/load、x0 写入、小立即数 `li`、`mv` 链等边界;确定性检查 129/129、幂等 33/33(对已调度输出再调度为不动点)。 +- 结论:第一轮的「调度保持语义」结论独立复现且样本更大;正确性不是本 topic 的短板。 + +#### 4.5.4 发射形式静态对照与上游隐患 + +- `sw sp(16), t0`(`base(offset)` 方言)被 pr-59 正确识别(12 条 spill store 全部可调度);topic_18 的 T3 缺口在 pr-59 不存在,这是 pr-59 的净优势。 +- **上游 linear 分配器 bug(F12)**:`LsInstruction.to_asm`(`regalloc_linear.py:116-126`)只打印操作数,把分支目标留在注释里,产物为 `bge a2, 4 # .Lloop_exit_3`、`j # .Lloop_header_1`;`RISCVAEncoder.assemble` 对前者直接 `IndexError`(greedy 路径编码正常)。后果:linear 输入被 `_unsafe_layout` 以 `numeric control-flow target` 整文件熔断,看起来像调度器不支持循环,实际是上游发射不完整。 +- `_unsafe_layout` 误报(F14):字符串常量含 `;`/`\`(如 `.asciz "a;b"`)即整文件零调度;`.popsection/.previous` 之后永久 `executable=False` 且无任何诊断;`.section .rodata,"ax"` 会因 flags 覆盖段名把只读段当可执行。 +- 后端合法方言 `max`、`slt rd, 0, rs`、`mv rd, 0` 等未建模(barrier),是 cnn 约 16.9% 覆盖率缺口的主要来源(与 F3 一致)。 + +#### 4.5.5 新增 Benchmark 文档与 CI 的核实(`dcf5f09`) + +- `docs/topic-18/18-指令调度器Benchmark.md`(本次重修时刚拉到的提交)**全部声称可复现**: + - 固定用例:`python3.11 -m benchmarks.run_inst_scheduler_case` 输出 PASS,表格逐项吻合(4 条指令、模型 5→4、停顿 1→0、编码 4 条/16B、TinyFive 执行 4 条、32 寄存器与 16 字内存一致);换序确为 `lw; addi; add; sw`。 + - CNN 静态 A/B:JSON 为 `assembly-ab`、`comparison_status=not_modeled`、`simulation.status=not_run`、`output_equal=null`;诊断 `numeric control-flow target; entire input preserved`;文档给出的 `bne t4, zero, -48` 实例存在于产物第 22 行。 + - 合成规模:数字与文档一致,5000 条 `skipped=1` 显示 `N/A`;JSON 新字段(`benchmark_type/input_sha256/seed/repeats/max_region_size/execution_verified=false`)齐全。 + - CI:三份 JSON/Markdown 已接入 workflow、Job Summary 与 artifact(`benchmark_reports/` 已被忽略)。 +- 小问题(F15):文档称"本地虚拟环境可使用 `.venv/bin/python`",但本机 `.venv` 为 Python 3.8,运行即崩在 `asm_parser_for_beautifier.py:35`;standalone CNN 生成命令本地直跑会 `ModuleNotFoundError`,需要 `pip install -e .` 或 `PYTHONPATH=.`(CI 靠安装步骤掩盖了这一点),文档未注明前提。 + +#### 4.5.6 topic_18 断言复核(供对照分支作者) + +T1(单条 `mul` 报 1 拍)、T2(`div + 独立 add` = 17 拍/15 stall,对照 pr-59 = 16 拍/0 stall)、T3(`sw sp(16), t0` 12 条钉死)、T6(benchmark 用 `r{c}_{i}` 非物理寄存器、legacy API、3 链 0%/单链 9.5%)、`ScheduleConfig.strict` 未接线、`except Exception` 兜底:**全部独立复现成立**;其测试 45 passed。原评审对 topic_18 的判断无需修改。 + +--- + +## 5. 发现清单(pr-59) + +级别定义:P0 = 正确性阻断;P1 = 合入前必修;P2 = 应当修复;P3 = 可延后。 + +| ID | 级别 | 位置 | 问题 | 证据 | 建议修复 | +|----|------|------|------|------|----------| +| F1 | P1 | 仓库根 `:memory:.ses`(提交 `ad9c806`) | 误提交 51B 会话残留(时间戳 + UUID),并把 `.gitignore` 中 `/docs/mytopic/` 删除 | `git show --stat ad9c806`:`+ :memory:.ses \| 2 +`;`git log pr-59 -- ':(literal):memory:.ses'` 指向该提交 | 删除文件;如为工具产物加 `.gitignore`;提交信息说明 | +| F2 | P1 | `docs/topic-18/18-指令调度器{设计文档,SPEC-Review,实现计划,代码说明}.md` | 4 份文档为不同时点的旧快照且互相冲突(文本入口 vs 禁止文本;fail-stop vs 安全降级;拒绝 linear+schedule vs 默认 linear;计划/说明称"尚未实现");引用的 3 份上游文档不在仓库 | `docs/topic-18/` 仅这 4 份;计划 §1 自认范围调整;说明开头"准备改还没有实现";设计 §4.3/§11 与代码相反 | 给文档加版本状态与本版差异说明;把"实现现状/验收口径"单独成节;补齐或移除失效引用 | +| F3 | P2 | `schedule_semantics.py:122-146,166-175`;`compiler.py:519-524` | 真实后端合法形式未建模:`max`(多指令展开,必须 barrier,无异议)、以字面量 0/1 作寄存器操作数的 `slt`/`mv` 等;`cnn.onnx` 10/59 未建模,且诊断仅 `info`,默认不可见 | 真实产物 line 11/22/33/51 `max`、12/23/34/67 `slt rd,0,rs`、53 `mv rd,0`、58 `slt rd,rd,1`;`riscv_encoder._reg_num` 把字面量静默映射为 x0(既有缺陷) | 上游让代码生成发射规范寄存器名(`zero`/`x0`);过渡期可把字面量 0 归一化为 x0(与后端编码一致);`--schedule-report`/stats 输出覆盖率,未建模数达阈值时升为 warning | +| F4 | P2 | `compiler.py:523` | `--schedule-report` 把多行报告塞进 `warnings` 列表,语义怪(warning 不是报告通道) | `warnings.append(result.report())` | 报告放 `stats["schedule"]["report"]` 或独立字段,CLI 单独打印 | +| F5 | P3 | `inst_scheduler.py:287-335,367-370` | 不安全布局熔断整个文件:一个 `.macro`/`;` 命中即全文件零调度;`skipped_regions=1` 与诊断行号固定为第 1 行,定位误导 | `test_unsafe_assembly_layout_preserves_entire_input`;诊断 `line 1` | 按区域/按行定位与熔断;至少把命中行号写进诊断 | +| F6 | P3 | `inst_scheduler.py:203-241,372-392` | 独立注释行/空行/`label: inst` 同行切分区域(保守但降低收益);注释与指令同行时可随行迁移,独立注释则不能 | `_read_lines` 对非指令行 `region += 1` | 评估"注释行随相邻指令绑定";明确文档化该保守策略 | +| F7 | P3 | `schedule_semantics.py:258-291`;`inst_scheduler.py:52-55,462-493` | 破坏性 API 变更:`SchedInst` 变 frozen 且忽略构造参数中的 def/use;`parse_instructions+build_dag` 跨区即抛错;`machine_instrs_from_scheduled` 对内存/终止/未知指令抛错 | 测试 `test_unknown_forms_are_explicit_boundaries`、`test_low_level_api_cannot_silently_drop_boundaries` | 在 CHANGELOG/模块 docstring 标注 breaking change 与迁移建议;考虑弃用排期 | +| F8 | P3 | `inst_scheduler.py:259-266,406-409` | `max_region_size=1024`:超限区域整体跳过(bench 5000 条 0 收益);大函数收益缺口 | benchmark 5000 行 `Skipped=1` | 后续可分窗/提高上限,或对超大区域仅做局部窗口调度 | +| F9 | P3 | `inst_scheduler.py:44-45,314`;`compiler.py:514-515` | 小问题合集:`latency_model={}` 与 `model=` 互斥判断过严;`.` 正则对字符串内 `;`/`\` 误报;`_run_asm_passes` 中后端校验死分支;两份 report API | 代码走读 + 用例 | 顺手清理,非阻塞 | +| F10 | P2 | `tests/test_inst_scheduler_integration.py:152-211`;`.github/workflows/ci.yml` | 最强安全证据(真实汇编 + qemu 执行对拍,19 例)在本环境与 CI 均 skip:CI 未安装 `qemu-riscv32`(ubuntu-latest 亦无);无性能/收益回归门 | `pytest -rs` 显示 19 个 skip;workflow 仅 `pip install pytest` | 增加安装 `qemu-user`/`gcc-riscv64-unknown-elf` 的 CI job,并让执行对拍成为必跑;对 benchmark 收益设最低阈值 | +| **F11** | **P1** | `schedule_model.py:43-68` | **div/rem 成本模型与真实微架构失配**(16 拍非阻塞 vs rocket 33 拍阻塞/e76 66 拍;mul 3 vs rocket 4):llvm-mca 合成语料只兑现模型声称的 8.1%,40/72 region 变慢,e76 整体净负 | §4.5.1;最小负例(size=100, seed=42, dep_chains=3):模型 240→187 vs llvm-mca 449→462 | div/rem 改为阻塞发射模型或禁止提前 div/rem;mul 校准为 4;模型按 CPU 可配置;把 llvm-mca A/B 纳入收益回归门控 | +| **F12** | **P1** | `regalloc_linear.py:116-126`(上游发射层) | linear 路径**分支目标只写注释不写操作数**,产物不可编码(`RISCVAEncoder` IndexError),并使调度器对含循环输入整文件熔断 | 产物 `bge a2, 4 # .Lloop_exit_3`、`j # .Lloop_header_1`;编码实测 IndexError,greedy 正常 | 修 `LsInstruction.to_asm` 让分支目标回到操作数位;补 linear 输出可编码回归测试;评估调度器是否可识别注释目标(次选) | +| **F13** | P2 | 真实负载覆盖(§4.5.2) | 真实可调度 region 平均 **1.86 条**,仅 3/24 文件有增益,动态指令数 0 变化;后端碎片化锁死收益上限 | 24 文件审计;region 尺寸分布 26×1、17×2…最大 5;官方合成块平均约 310 | 覆盖率与未建模数升级为 warning/报告字段;支持 `max` 与字面量操作数;评估合并小基本块后再调度 | +| **F14** | P2 | `inst_scheduler.py:287-335` | `_unsafe_layout` 误报与静默失效:字符串常量含 `;`/`\` 全文件熔断;`.popsection` 后永久不可调度且无诊断;`.section .rodata,"ax"` 把只读段当可执行 | §4.5.4 实测(`.asciz "a;b"` → 全文件保留;`.popsection` 后指令无诊断消失) | 熔断收窄到命中行/区域并做引号感知;`.popsection/.previous` 恢复可执行状态;段可执行性同时看段名与 flags | +| **F15** | P3 | `docs/topic-18/18-指令调度器Benchmark.md:7,38-42` | 本地运行说明有误:`.venv/bin/python`(3.8)运行即崩;standalone CNN 生成命令缺 `pip install -e .`/`PYTHONPATH=.` 前提 | 文档命令实测:3.8 崩于 `asm_parser_for_beautifier.py:35`;直跑脚本 `ModuleNotFoundError` | 更正 Python 版本口径;补充安装前提 | +| **F16** | P3 | `benchmarks/run_inst_scheduler_case.py:34-42` | 两套指令计数口径未对齐:`source_instructions=876` vs `scheduling.input_instructions=889`(`_op_/layer1/Conv:` 显示标签被调度器当未知指令计入),文档只说"源指令数不计显示标签" | §4.5.5 CNN JSON | 统一计数口径,或在文档与字段说明中显式注明差异 | + +**第二轮状态更新(第一轮 F1–F10)** + +- **F1 仍未闭环**:`:memory:.ses` 在第二轮 HEAD `035d180` 仍被 git 跟踪(`git ls-files` 命中;工作区干净),合入前必修。 +- **F2 部分改善**:新增 `18-指令调度器Benchmark.md` 质量高、口径诚实;但 4 份旧文档的状态/矛盾问题仍在,`18-指令调度器实现计划.md` 开头仍写"尚未修改实现代码"。 +- **F3 已被量化**:真实覆盖率见 §4.5.2;未建模根因新增一项"上游分支目标丢失"(F12)。 +- **F4/F5/F8/F9 结论不变**;**F6/F7 影响面较小**;**F10 仍未配置 CI 工具链**,但新 Benchmark 的固定用例已用 TinyFive 做了有界执行验证,部分弥补执行证据缺口。 +- 新增 **F11/F12 = P1 必修**,**F13/F14 = P2**,**F15/F16 = P3**。 + +--- + +## 6. 与 topic_18 的横向对比 + +### 6.1 对比总表 + +| 维度 | pr-59(Mastttttter) | topic_18(Seven Gao) | 胜者 | +|------|----------------------|----------------------|------| +| 结构 | 4 模块分层,单向依赖,语义一次性解析 | 单文件 1065 行,自包含 | pr-59(可测试/可演进) | +| 语义建模 | 精确白名单 + barrier,别名校验/立即数范围/两种内存语法 | OpcodeInfo 表 + 操作数宽容(int 字面量当合法操作数) | 各有千秋:pr-59 更严更稳,topic_18 覆盖后端字面量形式 | +| 后端语法覆盖 | 支持 `offset(base)` 与 `base(offset)` 两种,cnn.onnx 建模 49/59 | 只认 `offset(base)`,`sw sp(16), t0` 全部钉死(12/59)并刷 warning | **pr-59** | +| 依赖图 | RAW/WAR/WAW + memory + FP flags + control | RAW/WAR/WAW + memory chain + control | pr-59(FP 副作用序) | +| 成本模型 | 资源占用型(div 只占除法器),周期=完成时间;相对真实硬件仍偏乐观(F11) | 全局 EX 停摆(div 阻塞全流水 16 拍),周期=发射数(不含末条延迟) | **pr-59** | +| 收益门控 | `after < before` 严格更低才应用(同模型同口径) | 同思路,但模型偏差导致大量机会丢失 | pr-59 | +| 验证器 | 边序 + 独立读来源/最终写值 + 副作用序 + 对象同一/边界 | 置换 + 边序 + 末条 pinned(依赖自身 DAG) | **pr-59** | +| 失败处理 | 区域恢复 + 严格模式抛错 + 不写部分输出;只捕获自己的异常 | 区域 fallback + 警告;`except Exception` 兜底(与自身计划矛盾);strict 未接线 | pr-59 | +| 集成 | 配置校验 + `--schedule-strict/--schedule-report` + stats/诊断 | `_asm_pass_stats` 可变驱动状态 + `ScheduleConfig()`(strict 无效) | pr-59 | +| 测试 | 4 文件 **121 例**(精确断言 + 注入 + CRLF + fuzz 确定性 + 执行对拍 + 新 Benchmark 报告 16 例) | 1 文件 45 例(精确断言,无执行对拍) | pr-59 | +| 基准 | 重写为合法物理寄存器 + 生产入口 + 免责声明;第二轮新增执行验证/静态 A/B/合成三份 CI 报告 | 未更新(`r{c}_{i}` 伪寄存器、走 legacy API、3 链 0% 改善) | pr-59 | +| 文档 | 5 份:`Benchmark.md` 质量高且口径诚实;4 份旧快照互相矛盾(F2) | 无新增文档 | pr-59(Benchmark) | + +### 6.2 topic_18 的关键问题(供其分支作者修复) + +- **T1(P1)周期口径错误**:`estimate_sequence` 返回 `clock = 末条 issue + 1`,不含末条指令延迟。证据:单条 `mul` 报 **1 拍**(pr-59 报 3 拍);`lw; add` 两者一致纯属巧合。后果:报告收益被低估,且"把长延迟指令提前"这类收益被收益门控直接丢弃。 +- **T2(P1)全局 EX 停摆模型**:非流水除法把整条发射流水停 `latency` 拍(`ex_free = issue + exec_latency`)。证据:`div + 独立 add` 模型 **17 拍 / 15 stall**(pr-59 为 16 拍 / 0 stall);自带 benchmark 3 依赖链 0% 改善;对照语料无 div 时 0/400 次换序。这直接解释其"收益弱"的表象。 +- **T3(P1)后端语法缺口**:`sw sp(16), t0`(后端 Emitter 实际输出、编码器合法形式)不被 `_base_reg` 识别 → 12 条 spill store 全部锚定并输出 warning。在真实 CNN 产物上等于关闭了访存调度。 +- **T4(P2)验证器偏弱**:验证依赖同一实例构建的边表;不做"读取来源/最终写值"独立比对,也不校验对象同一与边界。 +- **T5(P2)集成毛刺**:`ScheduleConfig.strict` 在 `schedule_asm` 中未被使用;`compiler.py` 的 `except Exception` 兜底违反其自身计划 §8 的"不使用吞掉所有程序错误的通用 except";`_asm_pass_stats` 为可变驱动状态。 +- **T6(P2)基准未同步**:benchmark 仍用 `r{c}_{i}` 等非物理寄存器与 legacy API,未度量生产入口 `schedule_asm`;第二轮实测 3 依赖链全部 0% 改善、单链约 9.5%、5000 条压力测试 0% 且耗时 3.5s(对照 pr-59 生产入口重写版:同规模合成语料模型约 14%~19%,但对真实硬件同样偏乐观,见 F11)。 +- **T7(P3)语义边界**:`jalr ra, ...` 被当作 terminator(保守但语义上更像 call);label 名与寄存器 ABI 名同名时会被误判为寄存器(如名为 `ra` 的标签)。 + +### 6.3 可互相吸收的点 + +- topic_18 → pr-59:`_is_int_literal` 式操作数宽容(至少覆盖后端发出的 `slt rd, 0, rs`/`mv rd, 0`)可减少无谓 barrier;`_anchor_diagnostics` 把不支持形式即时告警的思路,可弥补 pr-59 info 级诊断不可见的问题。 +- pr-59 → topic_18:资源占用型模型、完成时间口径、独立验证器、strict 模式与配置校验、测试与基准的组织方式。 +- 合并策略:**同一 topic 不应同时落入两个实现**。建议以 pr-59 为合入版本,topic_18 转为参考实现或关闭;若团队偏好 topic_18 的自包含风格,至少要修完 T1~T3 再比较。 + +--- + +## 7. 测试与验证结论 + +### 7.1 通过项(第一轮 + 第二轮) + +- **pr-59 第二轮 HEAD `035d180`**:调度相关 **121 passed, 19 skipped**(含新增 `tests/test_inst_scheduler_report.py` 16 例);全仓 **848 passed, 19 skipped**。第一轮基线:调度器 105/105、全仓 760/760。 +- topic_18:45/45、594/594;第二轮独立复跑 45 passed。 +- **语义正确性(第二轮独立解释器)**:140 对差分执行、20022 条指令、**mismatch 0**;确定性 129/129、幂等 33/33(对已调度输出再调度为不动点)。 +- 故障注入、CRLF/EOF 保真、不安全整文件熔断、配置组合矩阵、`--schedule` 关闭时的 monkeypatch 哨兵均有用例锚定。 +- **新增 Benchmark(`dcf5f09`)独立复现**:固定用例 PASS 且 TinyFive 真实执行结果一致;CNN 静态 A/B 为 `not_modeled` 且两侧产物逐字节相同;合成 JSON 字段与数字一致;CI 已接入三份报告与 Job Summary。 +- 真实产物:`cnn.onnx` 调度后产物可编码,模型估算 59→55 拍(局部静态,非实测);llvm-mca 独立确认 4 个 region 合计约 +4 拍(rocket-rv32)。 + +### 7.2 覆盖缺口(第二轮更新) + +- **真实收益没有硬件或周期精确模拟背书**:llvm-mca 已给出独立微架构估计(合成语料仅兑现 8.1%、部分配置为负),但它仍是静态模型;无 qemu、无硬件计时(F10/F11)。 +- 真实汇编 + 执行对拍(19 例)在 CI 中仍默认跳过(F10);新 TinyFive 固定用例只覆盖 4 条直线整数代码,不能代表真实程序。 +- 未对 `ScheduleModel` 的延迟表做数值回归,也没有 benchmark 收益下限/负收益门控(F11)。 +- 未见 FP 语义的独立执行对拍(PR 内有 FP 用例,同样因 qemu 缺失 skip)。 +- 未测 `--schedule` 与 `--beautify/--peephole/--const-merge` 的顺序敏感组合之外的交叉影响(第二轮新增 standalone CNN `--const-merge` 后的静态 A/B,覆盖一种组合)。 + +--- + +## 8. 修复清单(供修复阶段执行,2026-09-15 第二轮更新) + +**合入前必修(P1)** + +- [ ] F1:删除 `:memory:.ses`;如系工具产物补 `.gitignore`;验证 `git status` 干净、CI 不受影响。 +- [ ] F11:修正 div/rem 成本模型——改为阻塞发射语义或禁止提前 div/rem;`mul` 由 3 校准为 4;先用 llvm-mca(rocket-rv32 / sifive-e76)对合成与真实语料跑 A/B,要求总收益为正、win ≥ loss,再对外声称"有收益"。验证:同一 region 的 llvm-mca A/B 不再出现系统性负优化。 +- [ ] F12:修 linear 分配器分支目标发射(`LsInstruction.to_asm`),使 linear 与 greedy 产物同样可编码;验证:`RISCVAEncoder.assemble` 对含循环 DSL 的 linear 产物能产出二进制,调度器不再因 `numeric control-flow target` 整文件熔断。 +- [ ] F2:为 `docs/topic-18/` 文档加状态页(设计=V2 backlog,计划/说明=历史快照,Benchmark=现状),修正"尚未修改实现代码"等与代码相反的描述。验证:文档内不再出现与实际代码相反的"当前实现"描述。 + +**紧跟改进(P2)** + +- [ ] F3:后端发射规范寄存器名;过渡期 `describe` 归一化字面量 `0`;覆盖率与未建模数写入 stats/report,超阈值升为 warning。 +- [ ] F4:报告移出 `warnings` 通道,CLI 单独打印。 +- [ ] F10:CI 增加 `qemu-user` + RISC-V 工具链,让 19 个执行对拍必跑;增加 llvm-mca 收益回归门(可与 F11 合并)。 +- [ ] F13:把"真实负载覆盖率"作为可量化指标跟踪;评估合并小基本块后再调度的可行性。 +- [ ] F14:`_unsafe_layout` 熔断收窄并做引号感知;修 `.popsection` 状态恢复与 `.section` flags 判断。 + +**延后(P3)** + +- [ ] F5/F6/F7/F8/F9:按 §5 建议排期,非合入阻塞。 +- [ ] F15:更正 Benchmark 文档的本地运行说明(Python 版本、`pip install -e .`/`PYTHONPATH` 前提)。 +- [ ] F16:统一 `source_instructions` 与 `scheduling.input_instructions` 计数口径,或在文档与字段说明中注明差异。 + +--- + +## 9. 综述评价 + +### 9.1 对 pr-59 的评价 + +**这是 topic 18 三版实现(旧文本重排 → topic_18 重写 → pr-59)里质量最高的一版。**它的价值不在算法复杂度(经典的块内列表调度),而在工程纪律: + +1. **先安全后收益**:未知形式不猜、危险布局熔断、收益不严格改善不应用、验证失败不落盘、strict 不写部分输出——把"面向教学编译器的可选优化"做成了可以放心开开关的部件;`main` 上那个会把汇编标签丢光的旧 `--schedule` 被彻底替换,这是"修复"最直观的证据。 +2. **语义精确性**:一型一解析、寄存器别名校验、正确识别多指令伪操作(`max`/大 `li`/分支立即数),避免了两类经典误编译(把伪操作当单指令调度、把字面量当寄存器)。 +3. **验证闭环**:独立验证器 + 顺序敏感共享模型 + 收益门控,使得"调度器自己的错"很难逃逸;这也是与其对照实现拉开差距的地方。 +4. **可观测、可测试、可演进**:stats/diagnostics/报告、121 个精确断言用例(含新增 Benchmark 报告 16 例)、四个单向依赖模块,为未来迁到 post-RA 结构化调度留了干净接口。 + +第二轮把「有效性」也纳入评审后,扣分项集中为三类:**合入卫生**(`:memory:.ses` 未删、四份文档矛盾未解)、**真实覆盖率**(真实可调度 region 平均 1.86 条,仅 3/24 文件有增益)、**模型可信度**(div/mul 与真实失配,llvm-mca 只兑现 8.1%,部分配置净负)。这些不涉及正确性(140 对差分执行 0 失败),但直接决定"这个开关在真实工作流里到底省了多少、什么时候不省",也决定它不应被当作性能达标项。综合评级(第二轮):**架构 A−,正确性 A,安全 A,测试 A−,有效性 D(模型内有效、真实未证实且可能为负),文档 B(新增 Benchmark 文档质量高,旧文档仍待整理),工程卫生 B**。 + +### 9.2 对 topic_18 的评价 + +自包含单文件的实现思路清晰、测试用例设计也不错(45 例,含精确断言与 `.label` 处理),但两个模型级问题(T1 周期口径、T2 全局 EX 停摆)和真实的语法缺口(T3)让它在同样的语料上基本"不敢动"——对照语料无 div 时 400 例 0 次换序(pr-59 为 95/400),自带 benchmark 3 链 0% 改善(pr-59 同规模语料约 14%~19% 模型节省;但第二轮 llvm-mca 表明 pr-59 的真实兑现远低于此,见 F11)。验证器与集成强度也不足。**它更适合作为参考实现**,其中"操作数宽容"和"即时不支持告警"两点值得吸收。 + +### 9.3 合并策略建议 + +1. 以 **pr-59** 为 topic 18 的合入版本,定位为"默认关闭的文本层 V1 基础设施";topic_18 不合入同一功能,保留分支存档或转为参考实现。 +2. 合入顺序:先完成 4 项 P1(F1 删残留文件、F11 div/rem 模型、F12 linear 分支目标、F2 文档状态),再合入;F3/F4/F10/F13/F14 紧随。 +3. 若短期内无法完成 F11/F12:建议暂缓合入,或合入但明确禁止对外宣称收益——一个默认关闭、开启后可能负优化的调度器对用户的伤害大于价值。 +4. 合入后必须在 `CHANGELOG` 标注:调度仍是文本层 V1、默认关闭、收益为局部静态模型估算、动态指令数不变、不代表硬件加速;post-RA 结构化调度列入 V2 backlog。 +5. **明确本 topic 与项目核心 KPI 的关系**:LLVM 对比的 4.2x 差距是动态指令数指标,而本调度器不改变指令数(24/24 文件 199→199),不应计入"超越 LLVM"的路径;它的价值在延迟优化基础设施与教学演示。 + +### 9.4 后续路线(在本次评审结论之上) + +- **短期**:F11 模型修正(div/rem 阻塞、mul=4)并用 llvm-mca 做收益门控;F1/F12/F2;F3/F10/F13/F14。让"真实覆盖率"和"llvm-mca A/B"变成 CI 可量化指标。 +- **中期**:修上游 linear 发射与分支目标;把 `SchedInst` 适配层接到结构化 `MachineInstr`(设计文档 §5 的 `InstructionView`),复用现有 model/verify/scheduler;统一 `PipelineCycleEstimator` 与新模型口径(设计 §18 的校准项)。 +- **长期**:post-RA/pre-emission 位置前移、内存别名放宽、issue width>1、按 CPU 校准模型、默认开启评估——都需要真实硬件校准与执行对拍支撑,不要以教学模型数字对外宣称硬件收益。 + +--- + +## 10. 最终结论:是否值得合入当前编译器(2026-09-15 第二轮) + +**结论:值得合入,但要按「默认关闭的 V1 基础设施」定位合入,并在合入前完成 F1/F11/F12/F2 四项 P1。** + +- **支持合入**:正确性已用独立解释器(140 对拍 0 失败)与全量测试(848 passed)充分验证;安全体系(精确语义 → 区域 → DAG → 独立验证器 → 收益门控 → strict/不写部分输出)是本仓库同类 pass 中最强一档,默认关闭时对主路径零风险;验证器、统计报告、CI 基准与 TinyFive 执行入口具备长期复用价值,是未来 post-RA 结构化调度的干净底座;新增 Benchmark 文档口径诚实,没有把模型数字当硬件收益。 +- **保留意见**:它目前不产生可证实的真实收益——llvm-mca 只兑现模型声称的 8.1%,40/72 region 反而变慢,sifive-e76 整体净负;真实负载 24 个输入只有 3 个发生换序;对动态指令数 0 贡献。若合入标准是"带来可测的性能提升",本 topic 不达标。 +- **分场景判定**: + - 作为「教育/基础设施特性」——**合入**(修完 P1); + - 作为「性能达标项 / 对外宣称超越 LLVM 的依据」——**不成立**,不应以此宣传; + - 在 P1(尤其 F11/F12)未修时——**不建议合入**,因为开启后可能让用户代码变慢。 + +**一句话**:这是一件正确、安全、诚实、但目前几乎不产生真实收益的基础设施;按默认关闭的 V1 合入并立即修模型,是风险与收益最平衡的选择。 diff --git a/scratchv/backend/__init__.py b/scratchv/backend/__init__.py index 035aeb7..4a03ffe 100644 --- a/scratchv/backend/__init__.py +++ b/scratchv/backend/__init__.py @@ -17,6 +17,7 @@ ) from .inst_scheduler import ( InstructionScheduler, parse_instructions, machine_instrs_from_scheduled, + ScheduleConfig, ScheduleResult, schedule_assembly, ) from .inst_select_ext import ExtendedInstructionSelector from .cycle_estimator import ( @@ -39,5 +40,6 @@ "AsmPeepholeOptimizer", "merge_constants", "LinearScanAllocator", "block_from_machine_instrs", "machine_instrs_from_block", "InstructionScheduler", "parse_instructions", "machine_instrs_from_scheduled", + "ScheduleConfig", "ScheduleResult", "schedule_assembly", "ExtendedInstructionSelector", ] diff --git a/scratchv/backend/inst_scheduler.py b/scratchv/backend/inst_scheduler.py index 72b44cd..d31afe5 100644 --- a/scratchv/backend/inst_scheduler.py +++ b/scratchv/backend/inst_scheduler.py @@ -1,551 +1,648 @@ -"""Instruction Scheduler for RISC-V (List Scheduling). +"""Safe local RISC-V scheduling over immutable instruction objects. -Reorders instructions within a basic block to reduce pipeline stalls -caused by data dependencies, using list scheduling with critical-path -priority. - -Usage:: - - from scratchv.backend.inst_scheduler import InstructionScheduler - sched = InstructionScheduler() - dag = sched.build_dag(instructions) - scheduled = sched.schedule(dag) +LLVM references: ScheduleDAGInstrs::addPhysRegDeps, SUnit::ComputeHeight, +MachineScheduler::getSchedRegions, PostRASchedulerList::ListScheduleTopDown. """ from __future__ import annotations import argparse -import re as _re +import heapq +import json +import re import sys +import time from dataclasses import dataclass, field -from typing import Optional - - -# --------------------------------------------------------------------------- -# Latency model -# --------------------------------------------------------------------------- - -# Default RISC-V latency model (cycles before result is available) -_DEFAULT_LATENCY: dict[str, int] = { - # Integer ALU - "add": 1, "addi": 1, "sub": 1, - "sll": 1, "srl": 1, "sra": 1, - "xor": 1, "or": 1, "and": 1, - "xori": 1, "ori": 1, "andi": 1, - "slli": 1, "srli": 1, "srai": 1, - "slt": 1, "sltu": 1, "slti": 1, "sltiu": 1, - # M extension - "mul": 3, - "mulh": 3, "mulhsu": 3, "mulhu": 3, - "div": 16, "divu": 16, - "rem": 16, "remu": 16, - # Memory loads (cache hit assumed) - "lw": 2, "lh": 2, "lb": 2, "lbu": 2, "lhu": 2, - # Memory stores (non-blocking for subsequent loads) - "sw": 0, "sh": 0, "sb": 0, - # Branches (resolved in decode in simple cores, 1 cycle otherwise) - "beq": 1, "bne": 1, "blt": 1, "bge": 1, - "bltu": 1, "bgeu": 1, - # Jumps - "j": 0, "jal": 0, "jalr": 0, "ret": 0, - # Pseudo - "li": 1, "mv": 1, "nop": 0, "call": 3, - "lui": 1, "auipc": 1, - "max": 1, # ScratchV pseudo -} - - -# --------------------------------------------------------------------------- -# Instruction representation -# --------------------------------------------------------------------------- - -@dataclass -class SchedInst: - """An instruction node for the scheduler. - - Attributes - ---------- - id: - Unique index in the input list. - opcode: - Instruction mnemonic. - operands: - List of operand strings. - defines: - Set of register names that this instruction writes. - uses: - Set of register names that this instruction reads. - raw_line: - Original assembly text line. - """ - 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) - raw_line: str = "" +from pathlib import Path +from typing import Sequence - def __repr__(self) -> str: - return (f"SchedInst({self.id}, {self.opcode}, " - f"def={self.defines}, use={self.uses})") +from ._asm_parser import ParsedAsmLine, parse_line +from .schedule_model import ScheduleModel, estimate_order +from .schedule_semantics import DIVIDE, SchedInst, ScheduleError, integer, register_name +from .schedule_verify import verify_schedule -# --------------------------------------------------------------------------- -# DAG node -# --------------------------------------------------------------------------- - -@dataclass +@dataclass(eq=False) class DAGNode: - """A node in the instruction dependency DAG. - - Attributes - ---------- - inst: - The instruction this node represents. - predecessors: - List of edges (pred_node, latency) that must complete before this node. - successors: - List of edges (succ_node, latency) that depend on this node. - scheduled: - Whether this node has been scheduled. - ready_time: - Earliest cycle this node can be issued. - priority: - Critical path length (used for list scheduling priority). - """ inst: SchedInst - predecessors: list[tuple["DAGNode", int]] = field(default_factory=list) - successors: list[tuple["DAGNode", int]] = field(default_factory=list) + predecessors: list[tuple[DAGNode, int]] = field(default_factory=list) + successors: list[tuple[DAGNode, int]] = field(default_factory=list) scheduled: bool = False ready_time: int = 0 priority: int = 0 + edge_kinds: dict[int, frozenset[str]] = field(default_factory=dict) - def __repr__(self) -> str: - return (f"DAGNode(id={self.inst.id}, {self.inst.opcode}, " - f"prio={self.priority}, pred={len(self.predecessors)})") - - -# --------------------------------------------------------------------------- -# Instruction scheduler -# --------------------------------------------------------------------------- class InstructionScheduler: - """List scheduler for RISC-V basic blocks. - - Parameters - ---------- - latency_model: - Dict mapping opcode strings to execution latency (cycles). - If None, uses the default RISC-V latency model. - - Usage:: - - sched = InstructionScheduler() - dag = sched.build_dag(instructions) - scheduled_insts = sched.schedule(dag) - # scheduled_insts is a list of SchedInst in scheduled order - """ - - def __init__(self, latency_model: Optional[dict[str, int]] = None): - self.latency_model: dict[str, int] = ( - latency_model if latency_model is not None - else dict(_DEFAULT_LATENCY) - ) - self._original_order: list[SchedInst] = [] + """Deterministic single-region scheduling using physical register effects.""" + + def __init__( + self, + latency_model: dict[str, int] | None = None, + model: ScheduleModel | None = None, + ): + if model is not None and latency_model: + raise ValueError("Choose a model or latency overrides, not both") + self.model = model or ScheduleModel(overrides=latency_model or {}) + self.latency_model = dict(self.model.overrides) self._nodes: list[DAGNode] = [] - self._schedule_time: int = 0 - - # ------------------------------------------------------------------ - # DAG construction - # ------------------------------------------------------------------ - - def build_dag(self, instructions: list[SchedInst]) -> list[DAGNode]: - """Build a dependency DAG from a list of instructions. - - Parameters - ---------- - instructions: - List of SchedInst objects. - - Returns - ------- - List of DAGNode objects representing the dependency DAG. - """ - self._original_order = list(instructions) - self._nodes = [] - - # Create nodes - id_to_node: dict[int, DAGNode] = {} - for inst in instructions: - node = DAGNode(inst=inst) - self._nodes.append(node) - id_to_node[inst.id] = node - - # Build dependency edges (RAW hazards) - # For each register, track the last instruction that defined it - last_def: dict[str, DAGNode] = {} - - for inst in instructions: - node = id_to_node[inst.id] - - # RAW: each use depends on the last definition - for use_reg in inst.uses: - if use_reg in last_def: - pred = last_def[use_reg] - latency = self._get_latency(pred.inst.opcode) - node.predecessors.append((pred, latency)) - pred.successors.append((node, latency)) - - # WAW: later definitions of same register depend on earlier - for def_reg in inst.defines: - if def_reg in last_def and last_def[def_reg] is not node: - pred = last_def[def_reg] - latency = self._get_latency(pred.inst.opcode) - node.predecessors.append((pred, latency)) - pred.successors.append((node, latency)) - - last_def[def_reg] = node - - # Compute priorities (critical path length from each node) - self._compute_priorities() - return self._nodes - - def _get_latency(self, opcode: str) -> int: - """Get the latency for an opcode.""" - return self.latency_model.get(opcode, 1) + def build_dag(self, instructions: Sequence[SchedInst]) -> list[DAGNode]: + """Record register, memory, FP effect and fixed-terminator order.""" + if len({i.id for i in instructions}) != len(instructions): + raise ScheduleError("Duplicate instruction IDs") + if len({i.region for i in instructions}) > 1: + raise ScheduleError("Multiple regions: use schedule_assembly()") + self._nodes = nodes = [DAGNode(i) for i in instructions] + edges: dict[tuple[int, int], tuple[int, set[str]]] = {} + + def edge(a: int, b: int, distance: int, kind: str) -> None: + if a == b: + return + if a > b: + raise ScheduleError("Backward dependency in original order") + old_distance, kinds = edges.get((a, b), (0, set())) + kinds.add(kind) + edges[a, b] = max(distance, old_distance), kinds + + writes: dict[str, int] = {} + reads: dict[str, set[int]] = {} + memory = fp = terminal = None + for index, inst in enumerate(instructions): + self.model.timing(inst) + if terminal is not None and not inst.terminator: + raise ScheduleError("Body instruction after a terminator") + for reg in inst.uses: + if reg in writes: + producer = writes[reg] + edge( + producer, + index, + self.model.timing(instructions[producer]).latency, + "RAW", + ) + reads.setdefault(reg, set()).add(index) + for reg in inst.defines: + if reg in writes: + producer = writes[reg] + distance = max( + 1, self.model.timing(instructions[producer]).latency + - self.model.timing(inst).latency, + ) + edge(producer, index, distance, "WAW") + for reader in reads.get(reg, ()): + edge(reader, index, 1, "WAR") + reads[reg] = set() + writes[reg] = index + if inst.effects.memory != "none": + if memory is not None: + edge(memory, index, 1, "memory") + memory = index + if inst.effects.fp_flags: + if fp is not None: + edge(fp, index, 1, "fp-effects") + fp = index + if inst.terminator: + if terminal is None: + for body in range(index): + edge(body, index, 1, "control") + else: + edge(terminal, index, 1, "control") + terminal = index + + # Division timing/dispatch behaviour differs substantially by target. + # Keep every div/rem on the same side of every other instruction until + # a target-specific policy is validated. Between anchors, normal list + # scheduling still applies. Only link each intervening interval once. + anchor = None + for index, inst in enumerate(instructions): + if anchor is not None: + edge(anchor, index, 1, "division-order") + if inst.opcode in DIVIDE: + for previous in range(0 if anchor is None else anchor + 1, index): + edge(previous, index, 1, "division-order") + anchor = index + for (a, b), (distance, kinds) in sorted(edges.items()): + nodes[a].successors.append((nodes[b], distance)) + nodes[b].predecessors.append((nodes[a], distance)) + nodes[b].edge_kinds[nodes[a].inst.id] = frozenset(kinds) + self._compute_priorities() + return nodes def _compute_priorities(self) -> None: - """Compute the critical path length (priority) for each node. - - Priority = longest path from this node to a leaf (no successors), - where edge weights are latencies. - """ - # Topological sort for reverse traversal - visited: set[int] = set() - order: list[DAGNode] = [] - - def _dfs(n: DAGNode) -> None: - if n.inst.id in visited: - return - visited.add(n.inst.id) - for succ, _ in n.successors: - _dfs(succ) - order.append(n) - - for node in self._nodes: - _dfs(node) - - # Compute priorities in reverse topological order - for node in reversed(order): - max_succ_prio = 0 - for succ, lat in node.successors: - max_succ_prio = max(max_succ_prio, succ.priority + lat) - node.priority = ( - max_succ_prio + self._get_latency(node.inst.opcode) + # Edges point forward in original order; visit successors first. + for node in reversed(self._nodes): + node.priority = max( + self.model.timing(node.inst).latency, + max((d + succ.priority for succ, d in node.successors), default=0), ) - # ------------------------------------------------------------------ - # List scheduling - # ------------------------------------------------------------------ - def schedule(self, dag: list[DAGNode]) -> list[SchedInst]: - """Perform list scheduling on the DAG. - - Parameters - ---------- - dag: - List of DAGNode objects (output of ``build_dag``). - - Returns - ------- - List of SchedInst in scheduled order. - """ - self._nodes = dag - self._schedule_time = 0 - - # Reset scheduling state - for node in self._nodes: + """Use a time-ordered pending queue and priority queues per resource.""" + if len({n.inst.id for n in dag}) != len(dag): + raise ScheduleError("Duplicate graph nodes") + members = set(dag) + if any(p not in members for n in dag for p, _ in n.predecessors): + raise ScheduleError("Dependency outside scheduling region") + remaining = {n: len(n.predecessors) for n in dag} + order = {n: index for index, n in enumerate(dag)} + timings = {n: self.model.timing(n.inst) for n in dag} + pending = [] + available: dict[str, list] = {} + resources: dict[str, int] = {} + for node in dag: node.scheduled = False node.ready_time = 0 - - # Ready queue: nodes with no unscheduled predecessors - # Priority queue ordered by: higher priority first, then original order - ready: list[DAGNode] = [] - result: list[SchedInst] = [] - - # Find initial ready nodes - for node in self._nodes: - if not node.predecessors or all( - not p.scheduled for p, _ in node.predecessors - ): - pass # we'll process below - - # Main scheduling loop - remaining = {id(node): node for node in self._nodes} + if not remaining[node]: + heapq.heappush(pending, (0, order[node], node)) clock = 0 - - while remaining: - # Find nodes whose predecessors are all scheduled - ready = [] - for node in remaining.values(): - if all(p.scheduled for p, _ in node.predecessors): - ready.append(node) - - if not ready: - # Deadlock: should not happen for acyclic DAG - break - - # Sort ready nodes by priority (descending), then original id - ready.sort(key=lambda n: (-n.priority, n.inst.id)) - - # Pick the best node - node = ready[0] + result = [] + while len(result) < len(dag): + while pending and pending[0][0] <= clock: + _, index, node = heapq.heappop(pending) + queue = available.setdefault(timings[node].resource, []) + heapq.heappush(queue, (-node.priority, node.ready_time, index, node)) + choices = [ + queue[0] + for resource, queue in available.items() + if queue and resources.get(resource, 0) <= clock + ] + if not choices: + future = [pending[0][0]] if pending else [] + future.extend( + resources[resource] + for resource, queue in available.items() + if queue and resources.get(resource, 0) > clock + ) + if not future: + raise ScheduleError("Dependency cycle or inconsistent graph") + clock = min(future) + continue + _, _, _, node = min(choices) + timing = timings[node] + heapq.heappop(available[timing.resource]) node.scheduled = True - node.ready_time = clock result.append(node.inst) - del remaining[id(node)] - - # Advance clock by the instruction's latency - clock += self._get_latency(node.inst.opcode) - - self._schedule_time = clock + resources[timing.resource] = clock + timing.occupancy + for succ, distance in node.successors: + if succ not in remaining: + raise ScheduleError("Successor outside scheduling region") + succ.ready_time = max(succ.ready_time, clock + distance) + remaining[succ] -= 1 + if remaining[succ] == 0: + heapq.heappush(pending, (succ.ready_time, order[succ], succ)) + clock += timings[node].issue_occupancy return result - # ------------------------------------------------------------------ - # Utility - # ------------------------------------------------------------------ - - def estimate_cycles(self, instructions: list[SchedInst]) -> int: - """Estimate total execution cycles for a sequence (naive in-order).""" - total = 0 - for inst in instructions: - total += self._get_latency(inst.opcode) - return total - - def report(self, original: list[SchedInst], - scheduled: list[SchedInst]) -> str: - """Return a comparison report between original and scheduled order.""" - orig_cycles = self.estimate_cycles(original) - sched_cycles = self.estimate_cycles(scheduled) - improvement = orig_cycles - sched_cycles - pct = (improvement / orig_cycles * 100) if orig_cycles > 0 else 0.0 - - lines = [] - lines.append("Instruction Scheduling Report") - lines.append(f" Original instructions: {len(original)}") - lines.append(f" Estimated cycles (original): {orig_cycles}") - lines.append(f" Estimated cycles (scheduled): {sched_cycles}") - lines.append( - f" Improvement: {improvement} cycles ({pct:.1f}%)" + def estimate_cycles(self, instructions: Sequence[SchedInst]) -> int: + return estimate_order(instructions, self.model).cycles + + def report( + self, original: Sequence[SchedInst], scheduled: Sequence[SchedInst] + ) -> str: + before = self.estimate_cycles(original) + after = self.estimate_cycles(scheduled) + return ( + f"Instruction Scheduling Report ({self.model.name}; local model estimate)\n" + f" Estimated cycles (original): {before}\n" + f" Estimated cycles (scheduled): {after}\n" + f" Improvement: {before - after} cycles" ) - lines.append(" Scheduled order:") - for i, inst in enumerate(scheduled): - ops = ", ".join(inst.operands) if inst.operands else "" - lines.append(f" {i}: {inst.opcode} {ops}".rstrip()) - return "\n".join(lines) - -# --------------------------------------------------------------------------- -# Assembly parsing helpers -# --------------------------------------------------------------------------- -_LINE_RE = _re.compile( - r'^\s*' - r'(?:[A-Za-z_.][A-Za-z0-9_.]*:\s*)?' - r'\.?(?P[a-zA-Z][a-zA-Z0-9.]*)?\s*' - r'(?P[^#]*)' -) +@dataclass(frozen=True) +class AssemblyLine: + parsed: ParsedAsmLine + ending: str + instruction: SchedInst | None + executable: bool + diagnostic: str = "" + + +def _code_outside_strings(raw: str) -> str: + """Mask quoted strings and remove comments before checking layout syntax.""" + code = [] + quoted = escaped = False + for char in raw: + if quoted: + code.append(" ") + if escaped: + escaped = False + elif char == "\\": + escaped = True + elif char == '"': + quoted = False + elif char == "#": + break + elif char == '"': + quoted = True + code.append(" ") + else: + code.append(char) + return "".join(code) + + +def _read_lines(asm_text: str) -> list[AssemblyLine]: + """Keep source layout separately from executable, typed instructions.""" + result = [] + region = 0 + section = (".text", True) + previous_section = None + section_stack = [] + section_flags = {".text": True} + previous_terminal = False + for index, raw in enumerate(asm_text.splitlines(keepends=True)): + text = raw.rstrip("\r\n") + ending = raw[len(text) :] + parsed = parse_line(text, index) + # Standalone compiler listings also have display labels containing '/'. + # Preserve these as boundaries rather than counting them as opcodes. + display = _code_outside_strings(text).strip() + if parsed.label is None and re.fullmatch(r"[^\s:]+:", display): + parsed = ParsedAsmLine(raw=text, label=display[:-1], lineno=index) + message = "" + if parsed.is_directive: + op = parsed.opcode + if op in {"text", "data", "bss", "rodata", "section", "pushsection"}: + if op == "pushsection": + section_stack.append(section) + name = ( + "." + op if op in {"text", "data", "bss", "rodata"} + else parsed.operands[0].strip('"') if parsed.operands else "" + ) + executable = section_flags.get( + name, name == ".text" or name.startswith(".text.") + ) + if op in {"section", "pushsection"} and len(parsed.operands) > 1: + executable = "x" in parsed.operands[1].strip('"') + # Explicitly named data stays pinned even with unusual flags. + if any(name == prefix or name.startswith(prefix + ".") + for prefix in (".data", ".bss", ".rodata", ".sdata", ".sbss")): + executable = False + section_flags[name] = executable + previous_section, section = section, (name, executable) + elif op == "popsection": + if section_stack: + previous_section, section = section, section_stack.pop() + else: + section = ("", False) + message = "unmatched .popsection; awaiting an explicit section" + elif op == "previous": + if previous_section is not None: + section, previous_section = previous_section, section + else: + section = ("", False) + message = ".previous has no prior section; awaiting an explicit section" + executable = section[1] + inst = None + if parsed.label or parsed.is_directive or not parsed.opcode: + region += 1 + if parsed.opcode and not parsed.is_directive and executable: + if previous_terminal: + probe = SchedInst(index, parsed.opcode, parsed.operands) + if not probe.terminator: + region += 1 + inst = SchedInst( + index, parsed.opcode, parsed.operands, raw_line=text, region=region + ) + previous_terminal = inst.terminator + if parsed.label or inst.effects.barrier_reason: + region += 1 + else: + previous_terminal = False + result.append(AssemblyLine(parsed, ending, inst, executable, message)) + return result def parse_instructions(asm_text: str) -> list[SchedInst]: - """Parse RISC-V assembly text into a list of SchedInst for scheduling. - - Parameters - ---------- - asm_text: - Raw RISC-V assembly text. + """Read instructions with region IDs. Use schedule_assembly to rewrite files. - Returns - ------- - List of SchedInst objects. + Non-instruction lines are excluded here, but region IDs preserve boundaries: + build_dag rejects instructions from multiple regions. """ - result: list[SchedInst] = [] - lines = asm_text.strip().split("\n") - idx = 0 - for line in lines: - stripped = line.strip() - code = stripped.split("#")[0].strip() - if not code: - continue - - m = _LINE_RE.match(stripped) - if m is None: - continue - - opcode = m.group("opcode") - if opcode is None: - continue - opcode = opcode.lower().lstrip(".") - - # Skip labels - if opcode.endswith(":"): - continue - # Skip assembler directives - if opcode.startswith("."): - continue - - operands_str = (m.group("operands") or "").strip() - operands = [ - o.strip() for o in operands_str.split(",") if o.strip() - ] - - # Classify operands as defines/uses - defines: set[str] = set() - uses: set[str] = set() - pure_ops: list[str] = [] - - for i, op in enumerate(operands): - pure_ops.append(op) - # First operand is usually the destination for ALU-type - if i == 0 and opcode not in ( - "sw", "sh", "sb", "beq", "bne", - "blt", "bge", "bltu", "bgeu", - "j", "jal", "ret", - ): - defines.add(op) - else: - # Extract register from memory operands like "16(sp)" - m2 = _re.match(r'\d+\((\w+)\)', op) - if m2: - uses.add(m2.group(1)) - elif (op.startswith("x") or op.startswith("a") or - op.startswith("t") or op.startswith("s") or - op.startswith("f")): - uses.add(op) - - # For stores, first operand is a use (value to store) - if opcode in ("sw", "sh", "sb"): - if operands and operands[0] in defines: - defines.remove(operands[0]) - uses.add(operands[0]) - - # Handle labels and import from existing backend - if opcode.startswith("."): - defines = set() - uses = set() - - result.append(SchedInst( - id=idx, - opcode=opcode, - operands=pure_ops, - defines=defines, - uses=uses, - raw_line=line, - )) - idx += 1 + return [ + line.instruction + for line in _read_lines(asm_text) + if line.instruction is not None + ] - return result +@dataclass(frozen=True) +class ScheduleConfig: + strict: bool = False + max_region_size: int = 1024 + model: ScheduleModel = field(default_factory=ScheduleModel) -def machine_instrs_from_scheduled( - scheduled: list[SchedInst], -) -> list: # list of MachineInstr - """Convert scheduled SchedInst list back to MachineInstr list. + def __post_init__(self) -> None: + if self.max_region_size < 1: + raise ValueError("max_region_size must be positive") - This enables the instruction scheduler's output to be consumed by - ``RegisterAllocator`` and ``AsmEmitter``. - Parameters - ---------- - scheduled: - List of SchedInst objects in scheduled order. +@dataclass +class ScheduleResult: + asm_text: str + stats: dict + diagnostics: list[dict] + changed: bool + + def report(self) -> str: + s = self.stats + return ( + f"Instruction Scheduling Report ({s['model']}; local static model estimate)\n" + f" Estimated cycles: {s['original_cycles']} -> {s['final_cycles']}\n" + f" Saved cycles: {s['saved_cycles']}; moved instructions: {s['moved_instructions']}\n" + f" Modeled instructions: {s['modeled_instructions']}/{s['input_instructions']}\n" + f" Coverage: {s['coverage_ratio']:.1%}; unmodeled: {s['unmodeled_instructions']}\n" + f" Region size: mean {s['mean_region_size']:.2f}, max {s['max_region_instructions']}\n" + f" Applied regions: {s['applied_regions']}; skipped: {s['skipped_regions']}\n" + " Regions assume ready inputs; this is not whole-program runtime." + ) - Returns - ------- - List of MachineInstr objects. - """ - from scratchv.backend.machine_types import MachineInstr, MachineOp, MachineOperand - result = [] - for inst in scheduled: - if inst.opcode == ".label": - result.append(MachineInstr( - MachineOp.LABEL, comment="", - )) - continue +def _unsafe_layout(lines: Sequence[AssemblyLine]) -> tuple[int, str] | None: + # These constructs can change the interpretation of later instructions. + unsafe = { + "macro", + "endm", + "rept", + "endr", + "irp", + "irpc", + "include", + "incbin", + "org", + "set", + "equ", + "equiv", + "if", + "ifdef", + "ifndef", + "else", + "endif", + } + for line in lines: + p = line.parsed + code = _code_outside_strings(p.raw) + if ( + ";" in code + or "\\" in code + or (p.opcode != "size" and re.search(r"(? ScheduleResult: + """Verify each candidate before applying it; errors restore the whole region.""" + config = config or ScheduleConfig() + sensitivity_model = config.model.sensitivity_model() + started = time.perf_counter() + lines = _read_lines(asm_text) + output = [line.parsed.raw + line.ending for line in lines] + diagnostics: list[dict] = [] + stats = { + "model": config.model.name, + "model_overrides": dict(config.model.overrides), + "division_policy": "preserve-relative-order; conservative blocking issue", + "sensitivity_model": sensitivity_model.name, + "sensitivity_overrides": dict(sensitivity_model.overrides), + "sensitivity_rejected_regions": 0, + "estimate_scope": "sum of local static regions; ready inputs; no cache/branch prediction", + "input_instructions": sum(line.instruction is not None for line in lines), + "modeled_instructions": 0, + "moved_instructions": 0, + "applied_regions": 0, + "skipped_regions": 0, + "original_cycles": 0, + "candidate_cycles": 0, + "final_cycles": 0, + "original_stalls": 0, + "final_stalls": 0, + "regions": [], + } + + def diagnostic(line: int, reason: str, severity: str = "info") -> None: + diagnostics.append({"line": line + 1, "reason": reason, "severity": severity}) - try: - mop = MachineOp(inst.opcode) - except ValueError: - mop = MachineOp.MV - - 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) + for line in lines: + if line.diagnostic: + diagnostic(line.parsed.lineno, line.diagnostic, "warning") + modeled_ids: set[int] = set() + unsafe = _unsafe_layout(lines) + if unsafe: + line, reason = unsafe + diagnostic(line, reason + "; entire input preserved", "warning") + stats["skipped_regions"] = 1 + else: + regions: list[list[SchedInst]] = [] + current: list[SchedInst] = [] + for line in lines: + inst = line.instruction + fixed = ( + inst is None + or line.parsed.label is not None + or bool(inst.effects.barrier_reason) + ) + if current and (fixed or inst.region != current[-1].region): + regions.append(current) + current = [] + if not fixed: + current.append(inst) + elif inst is not None: + diagnostic( + inst.id, + inst.effects.barrier_reason or "instruction shares a label line", + ) + if current: + regions.append(current) + for original in regions: + row = { + "start_line": original[0].id + 1, + "end_line": original[-1].id + 1, + "instructions": len(original), + "status": "unchanged", + "moved": 0, + "original_cycles": None, + "candidate_cycles": None, + "final_cycles": None, + "dependencies": {}, + } + stats["regions"].append(row) + if len(original) > config.max_region_size: + row["status"] = "skipped" + stats["skipped_regions"] += 1 + diagnostic(original[0].id, "region size exceeds configured limit") + continue try: - return MachineOperand.immediate(int(s)) - except ValueError: - return MachineOperand.vreg(s) - - ops = [_to_mop(o) for o in inst.operands] - dst = ops[0] if len(ops) >= 1 else None - src1 = ops[1] if len(ops) >= 2 else None - src2 = ops[2] if len(ops) >= 3 else None - - result.append(MachineInstr(mop, dst, src1, src2, inst.raw_line)) + scheduler = InstructionScheduler(model=config.model) + dag = scheduler.build_dag(original) + candidate = scheduler.schedule(dag) + verify_schedule(original, candidate, dag) + before = estimate_order(original, config.model) + after = estimate_order(candidate, config.model) + sensitivity_before = estimate_order(original, sensitivity_model) + sensitivity_after = estimate_order(candidate, sensitivity_model) + sensitive = (after.cycles < before.cycles + and sensitivity_after.cycles >= sensitivity_before.cycles) + applied = after.cycles < before.cycles and not sensitive + stats["sensitivity_rejected_regions"] += int(sensitive) + final = candidate if applied else original + verify_schedule(original, final, dag) + final_estimate = after if applied else before + row.update( + status="applied" if applied else "model_sensitive" if sensitive else "no_improvement", + original_cycles=before.cycles, + candidate_cycles=after.cycles, + final_cycles=final_estimate.cycles, + sensitivity_original_cycles=sensitivity_before.cycles, + sensitivity_candidate_cycles=sensitivity_after.cycles, + original_issue_cycles=list(before.issue_cycles), + candidate_issue_cycles=list(after.issue_cycles), + ) + for node in dag: + for kinds in node.edge_kinds.values(): + for kind in sorted(kinds): + row["dependencies"][kind] = ( + row["dependencies"].get(kind, 0) + 1 + ) + row["moved"] = sum(a is not b for a, b in zip(original, final)) + # Keep newline characters at the destination slots (including EOF). + for slot, inst in zip(original, final): + output[slot.id] = inst.raw_line + lines[slot.id].ending + stats["modeled_instructions"] += len(original) + modeled_ids.update(inst.id for inst in original) + stats["moved_instructions"] += row["moved"] + stats["applied_regions"] += int(applied) + stats["original_cycles"] += before.cycles + stats["candidate_cycles"] += after.cycles + stats["final_cycles"] += final_estimate.cycles + stats["original_stalls"] += before.stalls + stats["final_stalls"] += final_estimate.stalls + except ScheduleError as exc: + if config.strict: + raise ScheduleError(f"Line {original[0].id + 1}: {exc}") from exc + row["status"] = "restored" + row["reason"] = str(exc) + stats["skipped_regions"] += 1 + diagnostic(original[0].id, f"original order restored: {exc}", "warning") + result = "".join(output) + stats["saved_cycles"] = stats["original_cycles"] - stats["final_cycles"] + stats["output_instructions"] = stats["input_instructions"] + stats["unmodeled_instructions"] = stats["input_instructions"] - len(modeled_ids) + stats["coverage_ratio"] = ( + len(modeled_ids) / stats["input_instructions"] if stats["input_instructions"] else 0.0 + ) + counts: dict[str, int] = {} + for line in lines: + if line.instruction is not None and line.instruction.id not in modeled_ids: + op = line.instruction.opcode + counts[op] = counts.get(op, 0) + 1 + stats["unmodeled_by_opcode"] = dict(sorted(counts.items())) + sizes = [row["instructions"] for row in stats["regions"]] + stats["region_count"] = len(sizes) + stats["mean_region_size"] = sum(sizes) / len(sizes) if sizes else 0.0 + stats["max_region_instructions"] = max(sizes, default=0) + if stats["unmodeled_instructions"]: + first = next(line for line in lines + if line.instruction is not None and line.instruction.id not in modeled_ids) + diagnostic(first.parsed.lineno, + f"scheduling coverage {stats['coverage_ratio']:.1%}; " + f"{stats['unmodeled_instructions']}/{stats['input_instructions']} " + "instructions unmodeled; see unmodeled_by_opcode and diagnostics", + "warning") + stats["elapsed_seconds"] = time.perf_counter() - started + return ScheduleResult(result, stats, diagnostics, result != asm_text) + + +def machine_instrs_from_scheduled(scheduled: Sequence[SchedInst]) -> list: + """Legacy conversion for simple operands; reject lossy/unknown conversions.""" + from .machine_types import MachineInstr, MachineOp, MachineOperand + result = [] + for inst in scheduled: + if ( + inst.effects.barrier_reason + or inst.effects.memory != "none" + or inst.terminator + ): + raise ValueError( + "This instruction cannot be losslessly converted to MachineInstr" + ) + try: + opcode = MachineOp(inst.opcode) + except ValueError as exc: + raise ValueError(f"Unsupported MachineOp: {inst.opcode}") from exc + operands = [] + for value in inst.operands: + reg, imm = register_name(value), integer(value) + if reg is not None: + operands.append(MachineOperand.reg(reg)) + elif imm is not None: + operands.append(MachineOperand.immediate(imm)) + else: + raise ValueError(f"Unsupported machine operand: {value}") + if len(operands) > 3: + raise ValueError("Too many operands for MachineInstr") + operands.extend([None] * (3 - len(operands))) + result.append(MachineInstr(opcode, *operands)) return result -# --------------------------------------------------------------------------- -# CLI -# --------------------------------------------------------------------------- - def main() -> None: - """CLI entry point.""" parser = argparse.ArgumentParser( - description="RISC-V Instruction Scheduler (List Scheduling)", - ) - parser.add_argument( - "input", type=str, - help="Input assembly file (.s)", - ) - parser.add_argument( - "-o", "--output", type=str, default=None, - help="Output file (default: stdout)", + description="Safe local RISC-V instruction scheduling" ) - parser.add_argument( - "--report", action="store_true", - help="Print scheduling report to stderr", - ) - + parser.add_argument("input") + parser.add_argument("-o", "--output") + parser.add_argument("--report", action="store_true") + parser.add_argument("--strict", action="store_true") + parser.add_argument("--report-json") args = parser.parse_args() - - with open(args.input, "r") as f: - asm_text = f.read() - - instructions = parse_instructions(asm_text) - sched = InstructionScheduler() - dag = sched.build_dag(instructions) - scheduled = sched.schedule(dag) - - output_lines = [ - f" {inst.opcode} " + ", ".join(inst.operands) - for inst in scheduled - ] - result = "\n".join(output_lines) - + with open(args.input, newline="") as source: + asm_text = source.read() + try: + result = schedule_assembly(asm_text, ScheduleConfig(strict=args.strict)) + except ScheduleError as exc: + parser.exit(1, f"Scheduling failed: {exc}\n") if args.report: - print(sched.report(instructions, scheduled), file=sys.stderr) - + print(result.report(), file=sys.stderr) + for diagnostic in result.diagnostics: + print( + f"Schedule line {diagnostic['line']}: {diagnostic['reason']}", + file=sys.stderr, + ) + if args.report_json: + Path(args.report_json).write_text( + json.dumps( + {"stats": result.stats, "diagnostics": result.diagnostics}, indent=2 + ) + + "\n" + ) if args.output: - with open(args.output, "w") as f: - f.write(result) + with open(args.output, "w", newline="") as destination: + destination.write(result.asm_text) else: - print(result) + sys.stdout.write(result.asm_text) if __name__ == "__main__": diff --git a/scratchv/backend/machine_types.py b/scratchv/backend/machine_types.py index cebf64b..c2e7a0b 100644 --- a/scratchv/backend/machine_types.py +++ b/scratchv/backend/machine_types.py @@ -141,6 +141,32 @@ class MachineInstr: src2: Optional[MachineOperand] = None comment: str = "" + def __post_init__(self) -> None: + # Zero in an integer register source position has a canonical RISC-V + # representation. Keep immediate fields (li/addi/shifts) unchanged. + sources = { + MachineOp.ADD: ("src1", "src2"), + MachineOp.SUB: ("src1", "src2"), + MachineOp.MUL: ("src1", "src2"), + MachineOp.DIV: ("src1", "src2"), + MachineOp.REM: ("src1", "src2"), + MachineOp.XOR: ("src1", "src2"), + MachineOp.AND: ("src1", "src2"), + MachineOp.SLT: ("src1", "src2"), + MachineOp.MV: ("src1",), + MachineOp.ADDI: ("src1",), + MachineOp.SRAI: ("src1",), + MachineOp.BEQ: ("dst", "src1"), + MachineOp.BNE: ("dst", "src1"), + MachineOp.BLT: ("dst", "src1"), + MachineOp.BGE: ("dst", "src1"), + MachineOp.BNEZ: ("dst",), + } + for field_name in sources.get(self.op, ()): + operand = getattr(self, field_name) + if operand is not None and operand.kind == "imm" and operand.value == 0: + setattr(self, field_name, MachineOperand.reg("zero")) + def __repr__(self) -> str: parts = [self.op.value] for op in (self.dst, self.src1, self.src2): diff --git a/scratchv/backend/regalloc_linear.py b/scratchv/backend/regalloc_linear.py index 5cc645f..db1f4d6 100644 --- a/scratchv/backend/regalloc_linear.py +++ b/scratchv/backend/regalloc_linear.py @@ -108,6 +108,7 @@ class LsInstruction: defines: set[str] = field(default_factory=set) uses: set[str] = field(default_factory=set) comment: str = "" + target: str | None = None def __repr__(self) -> str: return (f"LsInstruction({self.id}, {self.opcode}, " @@ -115,9 +116,15 @@ def __repr__(self) -> str: def to_asm(self, rename: Optional[dict[str, str]] = None) -> str: """Emit this instruction as assembly after register renaming.""" + if self.opcode == ".label": + return f"{self.operands[0] if self.operands else self.comment}:" ops = self.operands[:] if rename: ops = [rename.get(o, o) for o in ops] + # MachineInstr currently stores direct control-flow targets in comment. + # Match AsmEmitter: targets are operands, never register-renamed. + if self.target: + ops.append(self.target) parts = [f" {self.opcode}"] if ops: parts.append(" " + ", ".join(ops)) @@ -571,7 +578,10 @@ def block_from_machine_instrs( op_str = str(op).lstrip("%") if op.kind == "vreg": # For the destination operand position - if op is mi.dst: + # Stores and conditional branches READ their first operand. + if op is mi.dst and mi.op.value not in { + "sw", "fsd", "fsw", "beq", "bne", "blt", "bge", "bnez", + }: defines.add(op_str) operands.append(op_str) else: @@ -586,13 +596,17 @@ def block_from_machine_instrs( comment=mi.comment, )) else: + target = mi.comment if mi.op.value in { + "j", "jal", "call", "bnez", "beq", "bne", "blt", "bge", + } else None result.append(LsInstruction( id=i, opcode=mi.op.value, operands=operands, defines=defines, uses=uses, - comment=mi.comment, + comment="" if target else mi.comment, + target=target, )) return result @@ -652,6 +666,6 @@ def _to_mop(s: str) -> MachineOperand: if len(ops) >= 3: src2 = ops[2] - result.append(MachineInstr(mop, dst, src1, src2, inst.comment)) + result.append(MachineInstr(mop, dst, src1, src2, inst.target or inst.comment)) return result diff --git a/scratchv/backend/schedule_model.py b/scratchv/backend/schedule_model.py new file mode 100644 index 0000000..48bb1c6 --- /dev/null +++ b/scratchv/backend/schedule_model.py @@ -0,0 +1,127 @@ +"""Single-issue teaching model shared by scheduling and order estimation. + +These are local static estimates, not hardware cycles or the existing +five-stage PipelineCycleEstimator's stage occupancy counts. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from types import MappingProxyType +from typing import Mapping, Sequence + +from .schedule_semantics import DIVIDE, LOADS, MULTIPLY, SchedInst, ScheduleError + + +@dataclass(frozen=True) +class Timing: + latency: int = 1 + resource: str = "alu" + occupancy: int = 1 + issue_occupancy: int = 1 + + +@dataclass(frozen=True) +class ScheduleModel: + # Conservative local model, not a complete model of any named CPU. + # Integer multiply/divide latencies follow LLVM's Rocket scheduling table; + # division movement is restricted separately in the dependency graph. + name: str = "scratchv-conservative-v2" + overrides: Mapping[str, int] = field(default_factory=dict) + + def __post_init__(self) -> None: + values = dict(self.overrides) + if any( + not isinstance(v, int) or isinstance(v, bool) or v < 1 + for v in values.values() + ): + raise ValueError("Scheduling latencies must be positive integers") + object.__setattr__(self, "overrides", MappingProxyType(values)) + + def sensitivity_model(self) -> ScheduleModel: + """Check gains against a second plausible integer latency table. + + LLVM's Rocket and SiFive7 tables differ in multiply/divide latency. + This deliberately retains our simplified issue/resource model; it is + a sensitivity check, not a complete simulation of either processor. + Explicit caller overrides take precedence in both estimates. + """ + return ScheduleModel( + name="scratchv-latency-sensitivity-v1", + overrides={ + **{op: 3 for op in MULTIPLY}, + **{op: 66 for op in DIVIDE}, + **self.overrides, + }, + ) + + def timing(self, inst: SchedInst) -> Timing: + if inst.effects.barrier_reason: + raise ScheduleError( + f"Cannot estimate {inst.opcode}: {inst.effects.barrier_reason}" + ) + op = inst.opcode + latency, resource, occupancy = 1, "alu", 1 + if inst.effects.memory != "none": + latency, resource = (2 if op in LOADS else 1), "memory" + elif op in MULTIPLY: + latency, resource = 4, "multiply" + elif op in DIVIDE: + latency, resource, occupancy = 33, "divide", 33 + elif inst.terminator: + resource = "branch" + elif op.startswith("f"): + resource = "float" + double = op.endswith(".d") or op.startswith("fcvt.d.") + stem = op.split(".")[0] + if stem in {"fdiv", "fsqrt"}: + latency = 16 if double else 12 + resource, occupancy = "float-divide", latency + elif stem in {"fmul", "fmadd", "fmsub", "fnmadd", "fnmsub"}: + latency = 5 if double else 4 + elif stem in {"fadd", "fsub", "fcvt"}: + latency = 4 if double else 3 + elif stem in {"fmin", "fmax", "feq", "flt", "fle"}: + latency = 3 if double else 2 + latency = self.overrides.get(op, latency) + if resource in {"divide", "float-divide"}: + occupancy = latency + return Timing(latency, resource, occupancy, latency if op in DIVIDE else 1) + + +@dataclass(frozen=True) +class OrderEstimate: + cycles: int + stalls: int + issue_cycles: tuple[int, ...] + + +def estimate_order( + instructions: Sequence[SchedInst], model: ScheduleModel | None = None +) -> OrderEstimate: + """Simulate this exact order, independently of graph/scheduler state.""" + model = model or ScheduleModel() + ready: dict[str, int] = {} + resources: dict[str, int] = {} + next_issue = completion = stalls = issue_blocked_until = 0 + issues = [] + for inst in instructions: + timing = model.timing(inst) + issue = max( + next_issue, + issue_blocked_until, + resources.get(timing.resource, 0), + max((ready.get(reg, 0) for reg in inst.uses), default=0), + # A younger write must not complete before an older write to the + # same physical register on this conservative in-order model. + max((ready.get(reg, 0) - timing.latency for reg in inst.defines), default=0), + ) + stalls += issue - next_issue + issues.append(issue) + for reg in inst.defines: + ready[reg] = issue + timing.latency + resources[timing.resource] = issue + timing.occupancy + completion = max(completion, issue + timing.latency) + next_issue = issue + 1 + issue_blocked_until = issue + timing.issue_occupancy + return OrderEstimate(completion, stalls, tuple(issues)) diff --git a/scratchv/backend/schedule_semantics.py b/scratchv/backend/schedule_semantics.py new file mode 100644 index 0000000..f37be47 --- /dev/null +++ b/scratchv/backend/schedule_semantics.py @@ -0,0 +1,294 @@ +"""Typed, conservative instruction effects for the assembly scheduler. + +Parsing happens here, once. Scheduling never interprets operand strings. +Unknown forms are boundaries, not instructions with guessed effects. +""" + +from __future__ import annotations + +import re +from dataclasses import dataclass, field + +from ._asm_parser import canonical_reg, is_integer_reg + +_FP_ALIASES = {f"ft{i}": f"f{i}" for i in range(8)} +_FP_ALIASES.update({"fs0": "f8", "fs1": "f9"}) +_FP_ALIASES.update({f"fa{i}": f"f{i + 10}" for i in range(8)}) +_FP_ALIASES.update({f"fs{i}": f"f{i + 16}" for i in range(2, 12)}) +_FP_ALIASES.update({f"ft{i}": f"f{i + 20}" for i in range(8, 12)}) + +INTEGER_BINARY = frozenset( + ["add", "sub", "sll", "srl", "sra", "xor", "or", "and", "slt", "sltu"] +) +MULTIPLY = frozenset(["mul", "mulh", "mulhu", "mulhsu"]) +DIVIDE = frozenset(["div", "divu", "rem", "remu"]) +INTEGER_IMMEDIATE = frozenset( + ["addi", "xori", "ori", "andi", "slti", "sltiu", "slli", "srli", "srai"] +) +LOADS = frozenset(["lb", "lbu", "lh", "lhu", "lw", "flw", "fld"]) +STORES = frozenset(["sb", "sh", "sw", "fsw", "fsd"]) +BRANCHES = frozenset( + ["beq", "bne", "blt", "bge", "bltu", "bgeu", "bgt", "ble", "bgtu", "bleu"] +) +ZERO_BRANCHES = frozenset(["beqz", "bnez", "bltz", "bgez", "blez", "bgtz"]) +_ROUNDING = frozenset(["rne", "rtz", "rdn", "rup", "rmm", "dyn"]) +_INTEGER = re.compile(r"[+-]?(?:0[xX][0-9a-fA-F]+|[0-9]+)\Z") +_LABEL = re.compile(r"(?:[A-Za-z_.$][\w.$]*|[0-9]+[fb])\Z") + + +def register_name(value: str) -> str | None: + """Resolve integer and FP ABI aliases, rejecting non-register operands.""" + value = value.strip().lower() + if is_integer_reg(value): + return canonical_reg(value) + value = _FP_ALIASES.get(value, value) + if re.fullmatch(r"f(?:[0-9]|[12][0-9]|3[01])", value): + return value + return None + + +def integer(value: str) -> int | None: + """Read a literal, without accepting symbols or assembler expressions.""" + if not _INTEGER.fullmatch(value): + return None + return int(value, 16 if "x" in value.lower() else 10) + + +@dataclass(frozen=True) +class MemoryAddress: + base: str + offset: int + + +def memory_address(value: str) -> MemoryAddress | None: + """Accept GAS offset(base) and the backend's historical base(offset).""" + match = re.fullmatch(r"\s*([^()]*)\(\s*([^()]*)\s*\)\s*", value) + if not match: + return None + left, right = (part.strip() for part in match.groups()) + base = register_name(right) + offset = integer(left or "0") + if base is None or not base.startswith("x") or offset is None: + base = register_name(left) + offset = integer(right or "0") + if base is None or not base.startswith("x") or offset is None: + return None + if not -2048 <= offset <= 2047: + return None + return MemoryAddress(base, offset) + + +@dataclass(frozen=True) +class InstructionEffects: + defines: frozenset[str] = frozenset() + uses: frozenset[str] = frozenset() + memory: str = "none" + address: MemoryAddress | None = None + control: str = "none" + target: str | None = None + fp_flags: bool = False + barrier_reason: str = "" + + +def describe(opcode: str, operands: tuple[str, ...]) -> InstructionEffects: + """Describe an exact supported form, or explain why it is pinned.""" + op = opcode.lower() + args = operands + + def boundary(reason: str, control: str = "none") -> InstructionEffects: + return InstructionEffects(control=control, barrier_reason=reason) + + def regs( + pattern: str, *, flags: bool = False, rounding: bool = False + ) -> InstructionEffects: + actual = args + # Floating rounding operands are optional; never guess extra operands. + if rounding and len(actual) == len(pattern) + 1 and actual[-1] in _ROUNDING: + actual = actual[:-1] + if len(actual) != len(pattern): + return boundary("unsupported operand form") + names = [register_name(arg) for arg in actual] + if any( + name is None or not name.startswith(kind.lower()) + for name, kind in zip(names, pattern) + ): + return boundary("invalid register operand") + return InstructionEffects( + defines=frozenset(names[:1]) - {"x0"}, + uses=frozenset(names[1:]) - {"x0"}, + fp_flags=flags, + ) + + if op in INTEGER_BINARY | MULTIPLY | DIVIDE: + return regs("xxx") + if op in INTEGER_IMMEDIATE: + if len(args) != 3: + return boundary("unsupported operand form") + imm = integer(args[2]) + limit = (0, 31) if op in {"slli", "srli", "srai"} else (-2048, 2047) + if imm is None or not limit[0] <= imm <= limit[1]: + return boundary("unsupported immediate") + dst, src = register_name(args[0]), register_name(args[1]) + if not dst or not src or not dst.startswith("x") or not src.startswith("x"): + return boundary("invalid register operand") + return InstructionEffects(frozenset({dst}) - {"x0"}, frozenset({src}) - {"x0"}) + if op in {"mv", "neg", "not", "seqz", "snez", "sltz", "sgtz"}: + return regs("xx") + if op == "nop" and not args: + return InstructionEffects() + if op in {"li", "lui"}: + if len(args) != 2: + return boundary("unsupported operand form") + dst, imm = register_name(args[0]), integer(args[1]) + low, high = (-2048, 2047) if op == "li" else (0, (1 << 20) - 1) + if not dst or not dst.startswith("x") or imm is None or not low <= imm <= high: + return boundary("unknown or multi-instruction immediate expansion") + return InstructionEffects(defines=frozenset({dst}) - {"x0"}) + if op in LOADS | STORES: + if len(args) != 2: + return boundary("unsupported memory form") + value, addr = args + if op in STORES and "(" in value: + addr, value = value, addr + address = memory_address(addr) + reg = register_name(value) + kind = "f" if op.startswith("f") else "x" + if address is None or not reg or not reg.startswith(kind): + return boundary("unsupported memory form") + load = op in LOADS + return InstructionEffects( + defines=(frozenset({reg}) - {"x0"}) if load else frozenset(), + uses=(frozenset({address.base}) | (frozenset() if load else {reg})) + - {"x0"}, + memory="read" if load else "write", + address=address, + ) + if op in BRANCHES | ZERO_BRANCHES: + count = 3 if op in BRANCHES else 2 + if len(args) != count or not _LABEL.fullmatch(args[-1]): + return boundary("unsupported branch target", "branch") + names = [register_name(arg) for arg in args[:-1]] + if any(name is None or not name.startswith("x") for name in names): + return boundary("invalid branch register", "branch") + return InstructionEffects( + uses=frozenset(names) - {"x0"}, control="branch", target=args[-1] + ) + if op == "j" and len(args) == 1 and _LABEL.fullmatch(args[0]): + return InstructionEffects(control="jump", target=args[0]) + if op == "ret" and not args: + return InstructionEffects(uses=frozenset({"x1"}), control="return") + if op == "jr" and len(args) == 1: + src = register_name(args[0]) + if src and src.startswith("x"): + return InstructionEffects(uses=frozenset({src}) - {"x0"}, control="jump") + if op == "jal": + if ( + len(args) == 2 + and register_name(args[0]) == "x0" + and _LABEL.fullmatch(args[1]) + ): + return InstructionEffects(control="jump", target=args[1]) + return boundary("call or unsupported jal", "call") + if op == "jalr": + # Only the no-link form can be a modeled terminator. Calls are barriers. + if len(args) == 3 and register_name(args[0]) == "x0": + src, offset = register_name(args[1]), integer(args[2]) + if ( + src + and src.startswith("x") + and offset is not None + and -2048 <= offset <= 2047 + ): + return InstructionEffects( + uses=frozenset({src}) - {"x0"}, control="jump" + ) + if len(args) == 2 and register_name(args[0]) == "x0": + src = register_name(args[1]) + if src and src.startswith("x"): + return InstructionEffects( + uses=frozenset({src}) - {"x0"}, control="jump" + ) + addr = memory_address(args[1]) + if addr: + return InstructionEffects( + uses=frozenset({addr.base}) - {"x0"}, control="jump" + ) + return boundary("call or unsupported jalr", "call") + if op in {"call", "tail"}: + return boundary("call boundary", "call") + if op.startswith("f"): + parts = op.split(".") + stem = parts[0] + if len(parts) == 2 and parts[1] in {"s", "d"}: + if stem in {"fadd", "fsub", "fmul", "fdiv", "fmin", "fmax"}: + return regs("fff", flags=True, rounding=stem not in {"fmin", "fmax"}) + if stem == "fsqrt": + return regs("ff", flags=True, rounding=True) + if stem in {"fmadd", "fmsub", "fnmadd", "fnmsub"}: + return regs("ffff", flags=True, rounding=True) + if stem in {"feq", "flt", "fle"}: + return regs("xff", flags=True) + if stem == "fclass": + return regs("xf") + if stem in {"fsgnj", "fsgnjn", "fsgnjx"}: + return regs("fff") + if stem in {"fmv", "fabs", "fneg"}: + return regs("ff") + if op in {"fmv.x.w", "fmv.x.s"}: + return regs("xf") + if op in {"fmv.w.x", "fmv.s.x"}: + return regs("fx") + if len(parts) == 3 and stem == "fcvt": + dst, src = parts[1:] + if ( + dst in {"s", "d", "w", "wu"} + and src in {"s", "d", "w", "wu"} + and (dst in {"s", "d"} or src in {"s", "d"}) + and dst != src + ): + return regs( + ("f" if dst in {"s", "d"} else "x") + + ("f" if src in {"s", "d"} else "x"), + flags=True, + rounding=True, + ) + return boundary("unknown opcode or unsupported form") + + +@dataclass(frozen=True) +class SchedInst: + """One immutable instruction, including its exact original source line. + + Legacy def/use constructor arguments are accepted, but recomputed from the + opcode description: caller hints must not override instruction semantics. + Registers in the resulting object use canonical xN/fN names. + """ + + id: int + opcode: str + operands: tuple[str, ...] = () + defines: frozenset[str] = frozenset() + uses: frozenset[str] = frozenset() + raw_line: str = "" + region: int = 0 + effects: InstructionEffects = field(init=False) + + def __post_init__(self) -> None: + object.__setattr__(self, "opcode", self.opcode.lower()) + object.__setattr__(self, "operands", tuple(self.operands)) + effects = describe(self.opcode, self.operands) + object.__setattr__(self, "effects", effects) + object.__setattr__(self, "defines", effects.defines) + object.__setattr__(self, "uses", effects.uses) + + @property + def terminator(self) -> bool: + return self.effects.control in {"branch", "jump", "return"} + + @property + def movable(self) -> bool: + return not self.effects.barrier_reason and not self.terminator + + +class ScheduleError(ValueError): + """An invalid schedule or an unsupported use of the low-level API.""" diff --git a/scratchv/backend/schedule_verify.py b/scratchv/backend/schedule_verify.py new file mode 100644 index 0000000..414d86b --- /dev/null +++ b/scratchv/backend/schedule_verify.py @@ -0,0 +1,51 @@ +"""Checks independent of graph construction and scheduling decisions.""" + +from __future__ import annotations + +from typing import Sequence + +from .schedule_semantics import SchedInst, ScheduleError + + +def _reads_and_final_writes(instructions: Sequence[SchedInst]) -> tuple[dict, dict]: + writes: dict[str, int] = {} + reads = {} + for inst in instructions: + for reg in inst.uses: + reads[inst.id, reg] = writes.get(reg) # None means the incoming value. + for reg in inst.defines: + writes[reg] = inst.id + return reads, writes + + +def verify_schedule( + original: Sequence[SchedInst], candidate: Sequence[SchedInst], dag: Sequence = () +) -> None: + """Reject changes to identity, boundaries, register values or effects.""" + expected = {inst.id: inst for inst in original} + actual = {inst.id: inst for inst in candidate} + if ( + len(expected) != len(original) + or len(actual) != len(candidate) + or expected.keys() != actual.keys() + ): + raise ScheduleError("Instruction identities lost or duplicated") + if any(actual[key] is not inst for key, inst in expected.items()): + raise ScheduleError("Instruction object or original text was replaced") + for before, after in zip(original, candidate): + if before.region != after.region: + raise ScheduleError("Instruction crossed a region boundary") + if (before.terminator or before.effects.barrier_reason) and before is not after: + raise ScheduleError("Pinned instruction moved") + if _reads_and_final_writes(original) != _reads_and_final_writes(candidate): + raise ScheduleError("Register read source or final write changed") + for effect in (lambda i: i.effects.memory != "none", lambda i: i.effects.fp_flags): + if [i.id for i in original if effect(i)] != [ + i.id for i in candidate if effect(i) + ]: + raise ScheduleError("Memory or floating-point effects reordered") + positions = {inst.id: index for index, inst in enumerate(candidate)} + for node in dag: + for pred, _ in node.predecessors: + if positions[pred.inst.id] >= positions[node.inst.id]: + raise ScheduleError("Dependency order violated") diff --git a/scratchv/compiler.py b/scratchv/compiler.py index 0b5f22d..40d4797 100644 --- a/scratchv/compiler.py +++ b/scratchv/compiler.py @@ -69,6 +69,8 @@ class CompilerConfig: peephole_asm: bool = False const_merge: bool = False schedule: bool = False + schedule_strict: bool = False + schedule_report: bool = False count_instr: bool = False cycle_stats: bool = False enable_forwarding: bool = True @@ -239,6 +241,11 @@ def compile(self, input_path: str, output_path: str | None = None, errors: list[str] = [] warnings: list[str] = [] + if self.config.schedule and self.config.backend != "riscv": + return CompileResult(success=False, errors=["--schedule requires the RISC-V backend"]) + if (self.config.schedule_strict or self.config.schedule_report) and not self.config.schedule: + return CompileResult(success=False, errors=["Scheduling options require --schedule"]) + # Resolve output path if output_path is None: output_path = "output.ll" if self.config.backend == "llvm" else "output.s" @@ -324,7 +331,13 @@ def compile(self, input_path: str, output_path: str | None = None, ) # --- 5. Post-codegen passes --- - asm_text = self._run_asm_passes(asm_text, warnings) + from scratchv.backend.schedule_semantics import ScheduleError + schedule_stats: dict = {} + try: + asm_text = self._run_asm_passes(asm_text, warnings, schedule_stats) + except ScheduleError as exc: + return CompileResult(success=False, errors=[f"Scheduling failed: {exc}"], + warnings=warnings, ir_dump=ir_dump) # --- 6. Cycle estimation --- cycle_report = "" @@ -353,7 +366,7 @@ def compile(self, input_path: str, output_path: str | None = None, output_text=asm_text, output_path=output_path, ir_dump=ir_dump, - stats={"opt_message": opt_message, "cycle_report": cycle_report}, + stats={"opt_message": opt_message, "cycle_report": cycle_report, **schedule_stats}, warnings=warnings, ) @@ -495,7 +508,8 @@ def _generate_riscv_dag(self, program) -> str: # ── Internal: post-codegen passes ─────────────────────────────────────── - def _run_asm_passes(self, asm_text: str, warnings: list[str]) -> str: + def _run_asm_passes(self, asm_text: str, warnings: list[str], + stats: dict | None = None) -> str: """Run assembly-level passes (peephole, const-merge, beautify, etc.).""" if self.config.peephole_asm: from scratchv.backend.asm_peephole import AsmPeepholeOptimizer @@ -510,26 +524,30 @@ def _run_asm_passes(self, asm_text: str, warnings: list[str]) -> str: if self.config.const_merge: from scratchv.backend.const_merge import merge_constants_detailed - asm_text, stats = merge_constants_detailed(asm_text) - if stats.total_changes: + asm_text, merge_stats = merge_constants_detailed(asm_text) + if merge_stats.total_changes: warnings.append( - f"Const merge: {stats.total_changes} changes " - f"({stats.merged_pairs} pairs, " - f"{stats.redundant_lui_removed} redundant lui)" + f"Const merge: {merge_stats.total_changes} changes " + f"({merge_stats.merged_pairs} pairs, " + f"{merge_stats.redundant_lui_removed} redundant lui)" ) if self.config.schedule: from scratchv.backend.inst_scheduler import ( - InstructionScheduler, parse_instructions, - ) - sched = InstructionScheduler() - insts = parse_instructions(asm_text) - dag = sched.build_dag(insts) - scheduled = sched.schedule(dag) - asm_text = "\n".join( - f" {inst.opcode} " + ", ".join(inst.operands) - for inst in scheduled + ScheduleConfig, ScheduleError, schedule_assembly, ) + if self.config.backend != "riscv": + raise ScheduleError("--schedule requires the RISC-V backend") + result = schedule_assembly(asm_text, ScheduleConfig(strict=self.config.schedule_strict)) + asm_text = result.asm_text + if stats is not None: + stats["schedule"] = {**result.stats, "diagnostics": result.diagnostics} + for diagnostic in result.diagnostics: + if diagnostic["severity"] == "warning": + warnings.append(f"Schedule line {diagnostic['line']}: {diagnostic['reason']}") + if self.config.schedule_report: + if stats is not None: + stats["schedule"]["report"] = result.report() if self.config.beautify_asm: from scratchv.backend.asm_beautifier import beautify_asm diff --git a/scratchv/main.py b/scratchv/main.py index 52feff5..013111d 100644 --- a/scratchv/main.py +++ b/scratchv/main.py @@ -92,6 +92,10 @@ def build_arg_parser() -> argparse.ArgumentParser: "--schedule", action="store_true", help="Run instruction scheduler (Topic 18)", ) + parser.add_argument("--schedule-strict", action="store_true", + help="Fail compilation on scheduler verification errors (requires --schedule)") + parser.add_argument("--schedule-report", action="store_true", + help="Print local scheduling model estimates (requires --schedule)") parser.add_argument( "--count-instr", action="store_true", help="Print instruction count statistics (Topic 12)", @@ -149,6 +153,8 @@ def args_to_config(args: argparse.Namespace) -> CompilerConfig: peephole_asm=args.peephole_asm, const_merge=args.const_merge, schedule=args.schedule, + schedule_strict=args.schedule_strict, + schedule_report=args.schedule_report, count_instr=args.count_instr, cycle_stats=args.cycle_stats, enable_forwarding=not args.no_forwarding, @@ -264,6 +270,8 @@ def main(argv: list[str] | None = None) -> int: print(f" optimizer: {result.stats['opt_message']}", file=sys.stderr) if result.stats.get("cycle_report"): print(result.stats["cycle_report"], file=sys.stderr) + if result.stats.get("schedule", {}).get("report"): + print(result.stats["schedule"]["report"], file=sys.stderr) # Verification if args.verify: diff --git a/tests/test_inst_scheduler.py b/tests/test_inst_scheduler.py index 5dfb38a..cfb8f5b 100644 --- a/tests/test_inst_scheduler.py +++ b/tests/test_inst_scheduler.py @@ -1,8 +1,12 @@ """Tests for Instruction Scheduler (List Scheduling).""" import pytest + from scratchv.backend.inst_scheduler import ( - InstructionScheduler, SchedInst, DAGNode, parse_instructions, + DAGNode, + InstructionScheduler, + SchedInst, + parse_instructions, ) @@ -10,30 +14,34 @@ class TestSchedInst: """Tests for SchedInst data class.""" def test_creation(self): - inst = SchedInst(0, "add", ["t0", "t1", "t2"], - defines={"t0"}, uses={"t1", "t2"}) + inst = SchedInst( + 0, "add", ["t0", "t1", "t2"], defines={"t0"}, uses={"t1", "t2"} + ) assert inst.id == 0 assert inst.opcode == "add" - assert inst.defines == {"t0"} - assert inst.uses == {"t1", "t2"} + assert inst.defines == {"x5"} + assert inst.uses == {"x6", "x7"} class TestDAGNode: """Tests for DAG node.""" def test_creation(self): - inst = SchedInst(0, "add", ["t0", "t1", "t2"], - defines={"t0"}, uses={"t1", "t2"}) + inst = SchedInst( + 0, "add", ["t0", "t1", "t2"], defines={"t0"}, uses={"t1", "t2"} + ) node = DAGNode(inst=inst) assert node.inst.opcode == "add" assert not node.scheduled assert node.priority == 0 def test_add_predecessor(self): - inst_a = SchedInst(0, "add", ["t0", "t1", "t2"], - defines={"t0"}, uses={"t1", "t2"}) - inst_b = SchedInst(1, "mul", ["t3", "t0", "t4"], - defines={"t3"}, uses={"t0", "t4"}) + inst_a = SchedInst( + 0, "add", ["t0", "t1", "t2"], defines={"t0"}, uses={"t1", "t2"} + ) + inst_b = SchedInst( + 1, "mul", ["t3", "t0", "t4"], defines={"t3"}, uses={"t0", "t4"} + ) na = DAGNode(inst=inst_a) nb = DAGNode(inst=inst_b) nb.predecessors.append((na, 2)) @@ -64,17 +72,16 @@ def test_parse_skip_labels_and_comments(self): def test_parse_defines_uses(self): asm = " add t0, t1, t2\n" insts = parse_instructions(asm) - assert "t0" in insts[0].defines - assert "t1" in insts[0].uses - assert "t2" in insts[0].uses + assert insts[0].defines == {"x5"} + assert insts[0].uses == {"x6", "x7"} def test_parse_store(self): asm = " sw t0, 0(sp)\n" insts = parse_instructions(asm) assert insts[0].opcode == "sw" # Store: first operand is a use (value to store) - assert "t0" in insts[0].uses - assert "sp" in insts[0].uses + assert insts[0].uses == {"x5", "x2"} + assert not insts[0].defines class TestInstructionScheduler: @@ -84,8 +91,7 @@ def test_build_dag_simple(self): insts = [ SchedInst(0, "li", ["t0", "42"], defines={"t0"}, uses=set()), SchedInst(1, "li", ["t1", "10"], defines={"t1"}, uses=set()), - SchedInst(2, "add", ["t2", "t0", "t1"], - defines={"t2"}, uses={"t0", "t1"}), + SchedInst(2, "add", ["t2", "t0", "t1"], defines={"t2"}, uses={"t0", "t1"}), ] scheduler = InstructionScheduler() dag = scheduler.build_dag(insts) @@ -104,38 +110,34 @@ def test_schedule_no_dependencies(self): scheduler = InstructionScheduler() dag = scheduler.build_dag(insts) scheduled = scheduler.schedule(dag) - assert len(scheduled) == 3 + assert [i.id for i in scheduled] == [0, 1, 2] def test_schedule_with_dependency(self): """An instruction that depends on a previous result.""" insts = [ SchedInst(0, "lw", ["t0", "0(a0)"], defines={"t0"}, uses={"a0"}), - SchedInst(1, "add", ["t1", "t0", "t2"], - defines={"t1"}, uses={"t0", "t2"}), + SchedInst(1, "add", ["t1", "t0", "t2"], defines={"t1"}, uses={"t0", "t2"}), SchedInst(2, "lw", ["t3", "4(a0)"], defines={"t3"}, uses={"a0"}), ] scheduler = InstructionScheduler() dag = scheduler.build_dag(insts) scheduled = scheduler.schedule(dag) - assert len(scheduled) == 3 + assert [i.id for i in scheduled] == [0, 2, 1] # The second lw is independent and may be moved before the add def test_estimate_cycles(self): insts = [ - SchedInst(0, "add", ["t0", "t1", "t2"], - defines={"t0"}, uses={"t1", "t2"}), - SchedInst(1, "mul", ["t3", "t0", "t4"], - defines={"t3"}, uses={"t0", "t4"}), + SchedInst(0, "add", ["t0", "t1", "t2"], defines={"t0"}, uses={"t1", "t2"}), + SchedInst(1, "mul", ["t3", "t0", "t4"], defines={"t3"}, uses={"t0", "t4"}), ] scheduler = InstructionScheduler() cycles = scheduler.estimate_cycles(insts) - # add(1) + mul(3) = 4 - assert cycles >= 2 + # add(1) + mul(4) = 5 in the conservative v2 model. + assert cycles == 5 def test_report(self): insts = [ - SchedInst(0, "add", ["t0", "t1", "t2"], - defines={"t0"}, uses={"t1", "t2"}), + SchedInst(0, "add", ["t0", "t1", "t2"], defines={"t0"}, uses={"t1", "t2"}), ] scheduler = InstructionScheduler() dag = scheduler.build_dag(insts) @@ -148,14 +150,11 @@ def test_priority_computation(self): """Verify that priorities are computed (critical path).""" insts = [ SchedInst(0, "lw", ["t0", "0(a0)"], defines={"t0"}, uses={"a0"}), - SchedInst(1, "mul", ["t1", "t0", "t2"], - defines={"t1"}, uses={"t0", "t2"}), + SchedInst(1, "mul", ["t1", "t0", "t2"], defines={"t1"}, uses={"t0", "t2"}), ] scheduler = InstructionScheduler() dag = scheduler.build_dag(insts) - # Both nodes should have priority > 0 - for node in dag: - assert node.priority > 0 + assert [node.priority for node in dag] == [6, 4] def test_empty_input(self): scheduler = InstructionScheduler() @@ -171,14 +170,14 @@ def test_custom_latency_model(self): def test_waw_dependency(self): """WAW: two writes to same register create a dependency.""" insts = [ - SchedInst(0, "add", ["t0", "t1", "t2"], - defines={"t0"}, uses={"t1", "t2"}), - SchedInst(1, "sub", ["t0", "t3", "t4"], - defines={"t0"}, uses={"t3", "t4"}), + SchedInst(0, "add", ["t0", "t1", "t2"], defines={"t0"}, uses={"t1", "t2"}), + SchedInst(1, "sub", ["t0", "t3", "t4"], defines={"t0"}, uses={"t3", "t4"}), ] scheduler = InstructionScheduler() dag = scheduler.build_dag(insts) assert len(dag) == 2 + assert dag[1].predecessors == [(dag[0], 1)] + assert dag[1].edge_kinds[0] == {"WAW"} if __name__ == "__main__": diff --git a/tests/test_inst_scheduler_integration.py b/tests/test_inst_scheduler_integration.py new file mode 100644 index 0000000..86fd26c --- /dev/null +++ b/tests/test_inst_scheduler_integration.py @@ -0,0 +1,297 @@ +"""Compiler/CLI integration and real RISC-V assembly execution comparisons.""" + +import json +import os +import random +import shutil +import subprocess +import sys +from collections import Counter + +import pytest + +from scratchv.backend.inst_scheduler import ( + InstructionScheduler, + ScheduleConfig, + schedule_assembly, +) +from scratchv.compiler import CompilerConfig, CompilerDriver +from scratchv.main import args_to_config, build_arg_parser + +SOURCE = "lw t0, 0(a0)\nadd t1, t0, t2\naddi t3, t3, 1\nret\n" +DSL = "x = add(a, b)\ny = mul(x, c)\nreturn y\n" + + +@pytest.mark.parametrize("allocator", ["linear", "greedy", "naive"]) +@pytest.mark.parametrize("dag", [False, True]) +def test_actual_compiler_paths_use_scheduler(tmp_path, allocator, dag): + config = CompilerConfig( + reg_alloc=allocator, + use_dag_isel=dag, + schedule=True, + schedule_strict=True, + schedule_report=True, + ) + driver = CompilerDriver(config) + original = driver._generate_code(driver._parse("", DSL)) + result = driver.compile("", str(tmp_path / "out.s"), dsl_source=DSL) + assert result.success, result.errors + assert result.stats["schedule"]["input_instructions"] > 0 + assert result.stats["schedule"]["modeled_instructions"] > 0 + assert "Scheduling Report" in result.stats["schedule"]["report"] + assert not any("Scheduling Report" in warning for warning in result.warnings) + # Preserve all lines supplied by code generation, including labels. + assert Counter(result.output_text.splitlines()) == Counter(original.splitlines()) + + +def test_disabled_has_identical_output_and_never_calls_scheduler(tmp_path, monkeypatch): + driver = CompilerDriver(CompilerConfig()) + expected = driver._generate_code(driver._parse("", DSL)) + + def forbidden(*args, **kwargs): + pytest.fail("disabled scheduling must not execute") + + monkeypatch.setattr("scratchv.backend.inst_scheduler.schedule_assembly", forbidden) + result = driver.compile("", str(tmp_path / "out.s"), dsl_source=DSL) + assert result.success + assert result.output_text == expected + assert "schedule" not in result.stats + + +def test_stats_are_per_compilation_and_strict_failure_leaves_output_intact( + tmp_path, monkeypatch +): + driver = CompilerDriver(CompilerConfig(schedule=True)) + monkeypatch.setattr(driver, "_generate_code", lambda program: SOURCE) + out = tmp_path / "out.s" + first = driver.compile("", str(out), dsl_source=DSL) + second = driver.compile("", str(out), dsl_source=DSL) + assert ( + first.stats["schedule"]["saved_cycles"] + == second.stats["schedule"]["saved_cycles"] + == 1 + ) + monkeypatch.setattr(InstructionScheduler, "schedule", lambda self, dag: []) + restored = driver.compile("", str(out), dsl_source=DSL) + assert restored.success + assert restored.output_text == SOURCE + assert any("restored" in warning for warning in restored.warnings) + out.write_text("existing output") + driver.config.schedule_strict = True + failed = driver.compile("", str(out), dsl_source=DSL) + assert not failed.success + assert "Scheduling failed" in failed.errors[0] + assert out.read_text() == "existing output" + + +@pytest.mark.parametrize( + "config", + [ + CompilerConfig(backend="llvm", schedule=True), + CompilerConfig(schedule_strict=True), + CompilerConfig(schedule_report=True), + ], +) +def test_invalid_options_fail_before_parsing_or_writing(tmp_path, config): + result = CompilerDriver(config).compile("does-not-exist", str(tmp_path / "out")) + assert not result.success + assert "schedule" in result.errors[0] + assert not (tmp_path / "out").exists() + + +def test_cli_flags_and_module_preserve_text_and_report_json(tmp_path): + args = build_arg_parser().parse_args( + ["--schedule", "--schedule-strict", "--schedule-report"] + ) + config = args_to_config(args) + assert config.schedule and config.schedule_strict and config.schedule_report + source, output, report = ( + tmp_path / name for name in ("in.s", "out.s", "report.json") + ) + source.write_bytes((".text\nmain:\n" + SOURCE).replace("\n", "\r\n").encode()) + result = subprocess.run( + [ + sys.executable, + "-m", + "scratchv.backend.inst_scheduler", + str(source), + "-o", + str(output), + "--strict", + "--report", + "--report-json", + str(report), + ], + capture_output=True, + text=True, + timeout=20, + check=False, + ) + assert result.returncode == 0, result.stderr + assert "Scheduling Report" in result.stderr + assert output.read_bytes().startswith(b".text\r\nmain:\r\n") + assert json.loads(report.read_text())["stats"]["saved_cycles"] == 1 + + +def test_composes_with_other_assembly_passes(): + driver = CompilerDriver( + CompilerConfig( + schedule=True, + schedule_strict=True, + peephole_asm=True, + const_merge=True, + beautify_asm=True, + count_instr=True, + ) + ) + stats, warnings = {}, [] + result = driver._run_asm_passes(".text\nf:\n" + SOURCE, warnings, stats) + assert "f:" in result and ".text" in result + assert stats["schedule"]["saved_cycles"] == 1 + + +@pytest.fixture +def execute_riscv(tmp_path): + """Use actual assembler/linker and CPU execution, never the IR executor.""" + if not shutil.which("clang") or not shutil.which("qemu-riscv32"): + if os.environ.get("SCRATCHV_REQUIRE_RISCV_EXECUTION") == "1": + pytest.fail("CI requires clang and qemu-riscv32; execution cannot be skipped") + pytest.skip("RISC-V execution checks require clang and qemu-riscv32") + sequence = 0 + + def execute(body, *, march="rv32imfd", prefix="", suffix=""): + nonlocal sequence + sequence += 1 + source = tmp_path / f"case{sequence}.s" + binary = tmp_path / f"case{sequence}" + # Dump all working integer registers, FP status and the shared data area. + # Code/link addresses are deliberately not treated as business outputs. + capture = "\n".join(f"sw t{i}, {i * 4}(a6)" for i in range(7)) + program = ( + ".text\n.globl _start\n_start:\nla a0, data\n" + + "\n".join(f"li t{i}, {i + 1}" for i in range(7)) + + "\n" + + prefix + + body + + "\nla a6, result\n" + + capture + + "\n" + "frflags a5\nsw a5, 28(a6)\n" + "li a0, 1\nla a1, result\nli a2, 96\nli a7, 64\necall\n" + "li a0, 0\nli a7, 93\necall\n" + suffix + "\n" + ".data\n.balign 8\nresult:\n.zero 32\ndata:\n" + + ".word 9, 11, 13, 15, 17, 19, 21, 23\n.zero 32\n" + ) + source.write_text(program) + compiled = subprocess.run( + [ + "clang", + "--target=riscv32-unknown-linux-gnu", + f"-march={march}", + "-mabi=ilp32d", + "-nostdlib", + "-static", + "-fuse-ld=lld", + "-Wl,--no-relax", + str(source), + "-o", + str(binary), + ], + capture_output=True, + text=True, + timeout=30, + check=False, + ) + assert compiled.returncode == 0, compiled.stderr + ran = subprocess.run( + ["qemu-riscv32", str(binary)], capture_output=True, timeout=10, check=False + ) + assert ran.returncode == 0, ran.stderr + assert len(ran.stdout) == 96 + return ran.stdout + + return execute + + +def test_linear_branch_target_executes_a_backward_loop(execute_riscv): + from scratchv.backend.machine_types import MachineInstr, MachineOp, MachineOperand + from scratchv.backend.regalloc_linear import LinearScanAllocator, block_from_machine_instrs + + counter = MachineOperand.vreg("counter") + block = block_from_machine_instrs([ + MachineInstr(MachineOp.LI, counter, MachineOperand.immediate(3)), + MachineInstr(MachineOp.LABEL, comment=".Lcounter"), + MachineInstr(MachineOp.ADDI, counter, counter, MachineOperand.immediate(-1)), + MachineInstr(MachineOp.BNEZ, counter, comment=".Lcounter"), + ]) + assembly = LinearScanAllocator(phys_regs=["t0"]).emit(block) + assert execute_riscv(assembly) == execute_riscv("li t0, 0\n") + + +@pytest.mark.parametrize( + "body", + [ + "lw t0, 0(a0)\nadd t1, t0, t2\naddi t3, t3, 1\n", + "add t1, t0, t2\nmul t0, t3, t4\nsw t0, 0(a0)\nlw t1, 0(a0)\n", + "sw t0, 0(a0)\nbeq t1, t1, done\nli t0, 99\ndone:\nadd t3, t0, t2\n", + ( + "li t5, 4\nloop:\nlw t0, 0(a0)\nadd t1, t0, t2\naddi t3, t3, 1\n" + "sw t1, 0(a0)\naddi t5, t5, -1\nbnez t5, loop\n" + ), + ( + "addi sp, sp, -16\nsw t0, 0(sp)\nlw t1, 0(sp)\nadd t2, t1, t3\n" + "mul t4, t5, t6\naddi sp, sp, 16\n" + ), + ( + "fcvt.s.w f0, t0\nfcvt.s.w f1, t1\nfdiv.s f2, f0, f1\n" + "addi t3, t3, 1\nfadd.s f3, f2, f0\nfsw f3, 0(a0)\nfcvt.w.s t4, f3, rtz\n" + ), + ( + "fcvt.d.w f0, t0\nfcvt.d.w f1, t1\nfdiv.d f2, f0, f1\n" + "addi t3, t3, 1\nfadd.d f3, f2, f0\nfsd f3, 0(a0)\nfcvt.w.d t4, f3, rtz\n" + ), + ], +) +def test_real_execution_preserves_registers_memory_control_and_float( + execute_riscv, body +): + result = schedule_assembly(body, ScheduleConfig(strict=True)) + assert execute_riscv(body) == execute_riscv(result.asm_text) + + +def test_call_boundary_and_return(execute_riscv): + body = "lw t0, 0(a0)\nadd t1, t0, t2\naddi t3, t3, 1\ncall helper\nadd t4, t0, t1\n" + helper = "helper:\nli t0, 17\nret\n" + result = schedule_assembly(body, ScheduleConfig(strict=True)) + assert result.changed + assert execute_riscv(body, suffix=helper) == execute_riscv( + result.asm_text, suffix=helper + ) + + +@pytest.mark.parametrize("seed", range(10)) +def test_random_legal_blocks_execute_equivalently(execute_riscv, seed): + rng = random.Random(seed) + body = SOURCE.replace("ret\n", "") + for _ in range(50): + dst, a, b = (f"t{rng.randrange(7)}" for _ in range(3)) + op = rng.choice(["add", "sub", "mul", "div", "rem", "lw", "sw"]) + if op in {"lw", "sw"}: + body += f"{op} {dst}, {rng.randrange(8) * 4}(a0)\n" + else: + body += f"{op} {dst}, {a}, {b}\n" + result = schedule_assembly(body, ScheduleConfig(strict=True)) + assert execute_riscv(body) == execute_riscv(result.asm_text) + + +def test_conv_machine_loop_executes_equivalently(execute_riscv): + # Small integer 1D convolution inner loop. Both load streams must keep order. + body = ( + "li t4, 0\nli t5, 4\nmv a3, a0\nconv:\n" + "lw t0, 0(a3)\nadd t1, t0, t2\nlw t3, 4(a3)\n" + "mul t6, t1, t3\nadd t4, t4, t6\naddi a3, a3, 4\n" + "addi t5, t5, -1\nbnez t5, conv\nsw t4, 0(a0)\n" + ) + result = schedule_assembly(body, ScheduleConfig(strict=True)) + assert result.changed + assert execute_riscv(body) == execute_riscv(result.asm_text) diff --git a/tests/test_inst_scheduler_report.py b/tests/test_inst_scheduler_report.py new file mode 100644 index 0000000..8b3360e --- /dev/null +++ b/tests/test_inst_scheduler_report.py @@ -0,0 +1,186 @@ +"""Report integration, execution evidence, and failed/zero-coverage cases.""" + +import hashlib +import json + +import pytest + +from benchmarks import bench_inst_scheduler as synthetic +from benchmarks import run_inst_scheduler_case as reports + + +def test_feature_report_uses_compiler_and_executes_equivalently(): + pytest.importorskip("tinyfive") + report = reports.run_case(reports.DEFAULT_CASE, repeats=1) + assert report["status"] == "passed" + assert report["feature"]["baseline_schedule"] is False + assert report["feature"]["compiler_config_schedule"] is True + assert report["feature"]["pipeline_matches_public_pass"] is True + assert report["feature"]["used"] is True + assert ( + report["input_sha256"] + == hashlib.sha256(reports.DEFAULT_CASE.read_bytes()).hexdigest() + ) + assert report["source_instructions"] == {"before": 4, "after": 4} + scheduling = report["scheduling"] + assert (scheduling["original_cycles"], scheduling["final_cycles"]) == (5, 4) + assert (scheduling["original_stalls"], scheduling["final_stalls"]) == (1, 0) + assert scheduling["moved_instructions"] == 2 + sim = report["simulation"] + assert sim["backend"] == "tinyfive" and sim["fallback"] is False + assert sim["output_equal"] and sim["registers_equal"] and sim["memory_equal"] + for result in (sim["before"], sim["after"]): + assert result["encoded_instructions"] == result["executed_instructions"] == 4 + assert result["code_size_bytes"] == 16 + assert len(result["all_registers"]) == 32 + assert result["all_registers"][5:8] == [9, 16, 7] + assert result["all_registers"][28] == 1 + assert result["memory_words"] == [9, 16] + [0] * 14 + markdown = reports._markdown(report) + assert "| 局部模型周期(估算) | 5 | 4 | 1 |" in markdown + assert "| TinyFive 实际执行指令数 | 4 | 4 | 0 |" in markdown + assert "不能证明硬件加速" in markdown + assert report["assembly"]["before"].rstrip() in markdown + assert report["assembly"]["after"].rstrip() in markdown + + +def test_static_ab_retains_zero_gain_and_never_simulates(tmp_path, monkeypatch): + source = b".text\r\nf:\r\n add t0, t1, t2\r\n add t3, t0, t4" + path = tmp_path / "zero-hit.s" + path.write_bytes(source) + + def forbidden(*args): + pytest.fail("static reports must not pretend to execute a real program") + + monkeypatch.setattr(reports, "_execute", forbidden) + report = reports.run_case(path, static_only=True, repeats=1) + assert report["status"] == "passed" + assert report["benchmark_type"] == "assembly-ab" + assert report["feature"]["used"] is False + assert report["scheduling"]["saved_cycles"] == 0 + assert report["scheduling"]["modeled_instructions"] == 2 + assert report["assembly"]["before"].encode() == source + assert report["assembly"]["after"].encode() == source + assert report["simulation"]["status"] == "not_run" + assert report["simulation"]["output_equal"] is None + assert "机器码大小、动态指令数及硬件耗时均未测量" in reports._markdown(report) + + +@pytest.mark.parametrize("source", ["custom t0, t1\n", "addi t0, t0, 1\n" * 1025]) +def test_unmodeled_inputs_are_not_reported_as_zero_cycle_runs(tmp_path, source): + path = tmp_path / "unmodeled.s" + path.write_text(source) + report = reports.run_case(path, static_only=True, repeats=1) + assert report["status"] == "passed" + assert report["scheduling"]["modeled_instructions"] == 0 + assert report["assembly"]["after"] == source + assert "| 局部模型周期(估算) | N/A | N/A | N/A |" in reports._markdown(report) + assert report["scheduling"]["diagnostics"] + + +def test_standalone_listing_labels_and_numeric_branches_are_reported_accurately( + tmp_path, +): + source = "_op_/layer1.0/Conv_5:\n lw t0, 0(a0)\n bne t1, zero, -4\n" + path = tmp_path / "cnn-listing.s" + path.write_text(source) + report = reports.run_case(path, static_only=True, repeats=1) + assert report["source_instructions"] == {"before": 2, "after": 2} + assert report["comparison_status"] == "not_modeled" + assert report["assembly"]["after"] == source + assert ( + "numeric control-flow target" + in report["scheduling"]["diagnostics"][0]["reason"] + ) + assert "0/2 条源指令" in reports._markdown(report) + + +def test_missing_simulator_fails_and_replaces_stale_reports(tmp_path, monkeypatch): + class UnavailableMachine: + def __init__(self, **kwargs): + self.available = False + + monkeypatch.setattr(reports, "ProfiledMachine", UnavailableMachine) + json_path, md_path = tmp_path / "report.json", tmp_path / "nested/report.md" + json_path.write_text('{"status":"passed"}') + status = reports.main( + ["--json", str(json_path), "--markdown", str(md_path), "--repeats", "1"] + ) + assert status == 1 + report = json.loads(json_path.read_text()) + assert report["status"] == "failed" + assert "fallback is forbidden" in report["error"] + assert "FAIL" in md_path.read_text() + + +def test_execution_mismatch_is_failed_with_evidence(tmp_path, monkeypatch): + pytest.importorskip("tinyfive") + execute = reports._execute + calls = 0 + + def corrupt_after(source): + nonlocal calls + calls += 1 + result = execute(source) + if calls == 2: + result["memory_words"][1] += 1 + return result + + monkeypatch.setattr(reports, "_execute", corrupt_after) + json_path, md_path = tmp_path / "report.json", tmp_path / "report.md" + status = reports.main( + ["--json", str(json_path), "--markdown", str(md_path), "--repeats", "1"] + ) + assert status == 1 + report = json.loads(json_path.read_text()) + assert report["status"] == "failed" + assert report["simulation"]["registers_equal"] is True + assert report["simulation"]["memory_equal"] is False + assert report["simulation"]["output_equal"] is False + assert "execution changed register or memory outputs" in md_path.read_text() + assert "| memory[1028] | 16 | 17 |" in md_path.read_text() + + +def test_feature_case_requires_actual_scheduling_gain(tmp_path): + pytest.importorskip("tinyfive") + path = tmp_path / "no-gain.s" + path.write_text("add t1, t0, t2\n") + report = reports.run_case(path, repeats=1) + assert report["status"] == "failed" + assert report["simulation"]["output_equal"] is True + assert "did not exercise" in report["errors"][0] + + +@pytest.mark.parametrize( + "source", + [ + "j loop\nloop:\nadd t0, t1, t2\n", + "fadd.s f0, f1, f2\n", + "li a0, 32\n", + "sw t0, 64(a0)\n", + "sw t0, 0(t1)\n", + ".data\n.word 1\n", + ], +) +def test_execution_harness_rejects_unsupported_programs(source): + with pytest.raises(ValueError): + reports._execute(source) + + +def test_synthetic_skip_is_explicit_and_not_execution_evidence(): + record = synthetic.bench_schedule( + synthetic._gen_instructions(5), repeats=1, max_region_size=4 + ) + assert record["benchmark_type"] == "synthetic" + assert record["modeled"] == 0 and record["skipped"] == 1 + assert record["execution_verified"] is False + markdown = synthetic._markdown([record]) + assert "N/A" in markdown and "未执行汇编" in markdown + + +def test_report_rejects_missing_compiler_integration(monkeypatch): + monkeypatch.setattr( + reports.CompilerDriver, "_run_asm_passes", lambda self, source, *args: source + ) + with pytest.raises(AssertionError, match="did not report scheduling statistics"): + reports.run_case(reports.DEFAULT_CASE, static_only=True, repeats=1) diff --git a/tests/test_inst_scheduler_review.py b/tests/test_inst_scheduler_review.py new file mode 100644 index 0000000..9cbaf54 --- /dev/null +++ b/tests/test_inst_scheduler_review.py @@ -0,0 +1,174 @@ +"""Regressions for the findings in docs/topic-18/topic18-review.md.""" + +from pathlib import Path + +import pytest + +from benchmarks.run_inst_scheduler_case import _instruction_count +from scratchv.backend.inst_scheduler import ( + InstructionScheduler, ScheduleConfig, parse_instructions, schedule_assembly, +) +from scratchv.backend.machine_types import MachineInstr, MachineOp, MachineOperand +from scratchv.backend.regalloc_linear import ( + LsInstruction, block_from_machine_instrs, machine_instrs_from_block, +) +from scratchv.backend.riscv_encoder import assemble_to_binary +from scratchv.backend.schedule_model import ScheduleModel, estimate_order +from scratchv.compiler import CompilerConfig, CompilerDriver +from scratchv.main import main + +CASE = "lw t0, 0(a0)\nadd t1, t0, t2\naddi t3, t3, 1\nsw t1, 4(a0)\n" + + +@pytest.mark.parametrize("literal", ['"a;b"', r'"a\\b"', '"."', '"a#;b"', r'"a\";b"']) +def test_quoted_data_does_not_disable_text_scheduling(literal): + source = f".data\nmessage: .asciz {literal}\n.text\n" + CASE + result = schedule_assembly(source, ScheduleConfig(strict=True)) + assert result.changed + assert result.asm_text.startswith(source[:source.index(".text")]) + assert result.stats["saved_cycles"] == 1 + + +@pytest.mark.parametrize("transition", [ + '.pushsection .rodata\n.word 1\n.popsection\n', + '.pushsection .rodata\n.pushsection .bss\n.zero 4\n.popsection\n.popsection\n', + '.section .rodata\n.word 1\n.previous\n', + '.section .rodata\n.word 1\n.previous\n.previous\n.previous\n', +]) +def test_section_state_restores_executable_code(transition): + source = ".text\n" + transition + CASE + result = schedule_assembly(source, ScheduleConfig(strict=True)) + assert result.changed + assert result.stats["input_instructions"] == 4 + assert result.stats["coverage_ratio"] == 1.0 + + +@pytest.mark.parametrize("section", ['.section .rodata,"ax"', '.section .text,"a"']) +def test_data_or_nonexecutable_flags_keep_text_pinned(section): + source = section + "\n" + CASE + assert schedule_assembly(source).asm_text == source + assert not parse_instructions(source) + + +def test_custom_executable_section_flags_survive_reselection(): + source = '.section .custom,"ax"\n.data\n.section .custom\n' + CASE + assert schedule_assembly(source).changed + + +@pytest.mark.parametrize("directive", [".popsection", ".previous"]) +def test_invalid_section_restore_is_visible_and_recovers(directive): + source = directive + "\n" + CASE + ".text\n" + CASE + result = schedule_assembly(source) + assert result.asm_text.startswith(directive + "\n" + CASE) + assert result.changed + assert result.diagnostics[0]["line"] == 1 + assert result.diagnostics[0]["severity"] == "warning" + + +def test_unsafe_layout_reports_actual_line_and_no_coverage(): + result = schedule_assembly(CASE + "j 8\n") + assert not result.changed + assert result.diagnostics[0]["line"] == 5 + assert result.stats["coverage_ratio"] == 0.0 + assert result.stats["unmodeled_instructions"] == 5 + + +def test_display_labels_and_noncode_have_one_counting_scope(): + source = ".text\n_op_/layer1/Conv:\n" + CASE + ".data\nadd t0, t1, t2\n" + result = schedule_assembly(source) + assert result.stats["input_instructions"] == _instruction_count(source) == 4 + assert result.stats["modeled_instructions"] == 4 + assert "_op_/layer1/Conv:\n" in result.asm_text + + +def test_partial_coverage_identifies_unsupported_opcodes(): + source = CASE + "max t0, t1, t2\nmv t0, 1\n" + result = schedule_assembly(source) + assert result.stats["unmodeled_by_opcode"] == {"max": 1, "mv": 1} + assert result.stats["coverage_ratio"] == 4 / 6 + assert result.stats["mean_region_size"] == 4 + assert any(d["severity"] == "warning" for d in result.diagnostics) + assert "unmodeled: 2" in result.report() + + +def test_waw_completion_is_part_of_the_cost_model(): + instructions = parse_instructions("mul t0, t1, t2\nli t0, 1\n") + assert estimate_order(instructions).issue_cycles == (0, 3) + graph = InstructionScheduler().build_dag(instructions) + assert graph[1].predecessors == [(graph[0], 3)] + + +def test_latency_sensitive_gain_is_rejected(): + source = "sw t0, 8(a0)\nmul t1, t2, t3\nlw t4, 0(a0)\n" + result = schedule_assembly(source, ScheduleConfig(strict=True)) + assert result.asm_text == source + assert result.stats["sensitivity_rejected_regions"] == 1 + row = result.stats["regions"][0] + assert (row["original_cycles"], row["candidate_cycles"], row["final_cycles"]) == (5, 4, 5) + assert row["sensitivity_original_cycles"] == row["sensitivity_candidate_cycles"] == 4 + assert row["status"] == "model_sensitive" + + +@pytest.mark.parametrize("op", ["div", "divu", "rem", "remu"]) +def test_no_instruction_can_cross_a_division(op): + source = f"li t4, 1\n{op} t0, t1, t2\nli t3, 2\n" + scheduler = InstructionScheduler() + instructions = parse_instructions(source) + assert [i.id for i in scheduler.schedule(scheduler.build_dag(instructions))] == [0, 1, 2] + assert schedule_assembly(source).asm_text == source + + +def test_empty_legacy_latency_overrides_accept_an_explicit_model(): + model = ScheduleModel(overrides={"mul": 7}) + assert InstructionScheduler(latency_model={}, model=model).model is model + + +def test_report_has_its_own_cli_channel(tmp_path, capsys): + source = tmp_path / "case.dsl" + source.write_text("x = add(a, b)\nreturn x\n") + status = main([str(source), "-o", str(tmp_path / "out.s"), "--schedule", "--schedule-report"]) + output = capsys.readouterr().err + assert status == 0 + assert "\nInstruction Scheduling Report" in output + assert "note: Instruction Scheduling Report" not in output + + +@pytest.mark.parametrize("filename", ["014_for_dot.dsl", "017_while_sum.dsl", "019_nested_loop.dsl"]) +@pytest.mark.parametrize("enabled", [False, True]) +def test_linear_control_flow_is_encoded_and_has_symbolic_targets(tmp_path, filename, enabled): + source = Path(__file__).parents[1] / "benchmarks" / "cases" / filename + result = CompilerDriver(CompilerConfig(reg_alloc="linear", schedule=enabled)).compile( + str(source), str(tmp_path / "out.s"), + ) + assert result.success, result.errors + binary = assemble_to_binary(result.output_text) + assert len(binary) > 0 and len(binary) % 4 == 0 + assert ".label" not in result.output_text + targets = [i.effects.target for i in parse_instructions(result.output_text) if i.effects.target] + assert targets + assert all(f"{target}:" in result.output_text for target in targets) + if enabled: + assert not any("numeric control-flow target" in d["reason"] + for d in result.stats["schedule"]["diagnostics"]) + + +def test_linear_target_is_not_renamed_and_first_branch_operand_is_a_use(): + v = MachineOperand.vreg + machine = MachineInstr(MachineOp.BNE, v("condition"), v("other"), comment="condition") + block = block_from_machine_instrs([machine]) + assert block[0].uses == {"condition", "other"} + assert not block[0].defines + assert block[0].to_asm({"condition": "t0", "other": "t1"}).strip() == "bne t0, t1, condition" + restored = machine_instrs_from_block(block)[0] + assert restored.comment == "condition" + explicit = LsInstruction(0, "j", ["target"], comment="an ordinary comment") + assert explicit.to_asm().strip() == "j target # an ordinary comment" + + +def test_zero_sources_are_canonical_but_immediates_keep_their_meaning(): + r, n = MachineOperand.reg, MachineOperand.immediate + assert MachineInstr(MachineOp.SLT, r("t0"), n(0), r("t1")).src1 == r("zero") + assert MachineInstr(MachineOp.MV, r("t0"), n(0)).src1 == r("zero") + immediate = MachineInstr(MachineOp.ADDI, r("t0"), n(0), n(0)) + assert immediate.src1 == r("zero") and immediate.src2 == n(0) + assert MachineInstr(MachineOp.LI, r("t0"), n(0)).src1 == n(0) diff --git a/tests/test_inst_scheduler_safety.py b/tests/test_inst_scheduler_safety.py new file mode 100644 index 0000000..9b5c542 --- /dev/null +++ b/tests/test_inst_scheduler_safety.py @@ -0,0 +1,314 @@ +"""Exact dependencies, scheduling times, source preservation and failure paths.""" + +import random +from dataclasses import FrozenInstanceError + +import pytest + +from scratchv.backend.inst_scheduler import ( + InstructionScheduler, + SchedInst, + ScheduleConfig, + ScheduleError, + machine_instrs_from_scheduled, + parse_instructions, + schedule_assembly, +) +from scratchv.backend.schedule_model import ScheduleModel, estimate_order +from scratchv.backend.schedule_verify import verify_schedule + +EXAMPLE = "lw t0, 0(a0)\nadd t1, t0, t2\naddi t3, t3, 1\nret\n" +EXPECTED = "lw t0, 0(a0)\naddi t3, t3, 1\nadd t1, t0, t2\nret\n" + + +@pytest.mark.parametrize( + "source,reads,writes", + [ + ("lw t0, -4(sp)", {"x2"}, {"x5"}), + ("lw x0, 0x10(fp)", {"x8"}, set()), + ("sw t0, 0(a0)", {"x5", "x10"}, set()), + ("sw sp(-4), t0", {"x2", "x5"}, set()), + ("lw t0, sp(-4)", {"x2"}, {"x5"}), + ("add t0, x5, zero", {"x5"}, {"x5"}), + ("ret", {"x1"}, set()), + ("beq t0, t1, target", {"x5", "x6"}, set()), + ("jalr zero, 0(ra)", {"x1"}, set()), + ("jalr zero, ra", {"x1"}, set()), + ("fsw fa0, 4(a0)", {"f10", "x10"}, set()), + ("fld fs0, (sp)", {"x2"}, {"f8"}), + ("fadd.s fa0, ft0, fs0, rtz", {"f0", "f8"}, {"f10"}), + ("fcvt.w.s a0, fa0, rtz", {"f10"}, {"x10"}), + ("fmv.s.x ft0, t0", {"x5"}, {"f0"}), + ("fmadd.d fa0, fa1, fa2, fa3", {"f11", "f12", "f13"}, {"f10"}), + ], +) +def test_exact_instruction_objects(source, reads, writes): + inst = parse_instructions(source)[0] + assert not inst.effects.barrier_reason + assert inst.uses == reads + assert inst.defines == writes + assert inst.raw_line == source + with pytest.raises(FrozenInstanceError): + inst.opcode = "nop" + + +@pytest.mark.parametrize( + "source", + [ + "custom t0, t1", + "call f", + "jal f", + "jal ra, f", + "jalr ra, t0, 0", + "ecall", + "fence", + "csrr t0, fflags", + "amoadd.w t0, t1, (a0)", + "li t0, 4098", + "li.d f0, 1.0", + "la t0, label", + "auipc t0, 1", + "addi t0, t1, %lo(symbol)", + "lw t0, broken", + "add %v0, t0, t1", + "add t0, t1", + "slli t0, t1, 32", + "fld f32, 0(a0)", + "fmin.s f0, f1, f2, rtz", + "feq.s t0, f1, f2, rtz", + ], +) +def test_unknown_forms_are_explicit_boundaries(source): + inst = parse_instructions(source)[0] + assert inst.effects.barrier_reason + result = schedule_assembly(EXAMPLE + source + "\n" + EXAMPLE) + assert result.asm_text == EXPECTED + source + "\n" + EXPECTED + assert result.diagnostics + + +def graph(source): + return InstructionScheduler().build_dag(parse_instructions(source)) + + +def test_three_register_dependencies_and_deduplication(): + nodes = graph("add t1, t0, t2\nmul t0, t3, t4\nadd t0, t0, t1") + assert nodes[1].predecessors == [(nodes[0], 1)] + assert nodes[1].edge_kinds[0] == {"WAR"} + assert nodes[2].predecessors == [(nodes[0], 1), (nodes[1], 4)] + assert nodes[2].edge_kinds[1] == {"RAW", "WAW"} + assert nodes[2].edge_kinds[0] == {"RAW"} + + +def test_all_readers_must_precede_overwrite(): + nodes = graph("add t1, t0, t2\nsub t3, t0, t4\nli x5, 9") + assert nodes[2].predecessors == [(nodes[0], 1), (nodes[1], 1)] + + +def test_zero_has_no_register_dependencies_but_load_still_has_memory_effect(): + nodes = graph("lw zero, 0(a0)\nadd t0, x0, t1\nsw t2, 4(a0)") + assert nodes[1].predecessors == [] + assert nodes[2].predecessors == [(nodes[0], 1)] + assert nodes[2].edge_kinds[0] == {"memory"} + + +@pytest.mark.parametrize( + "first,second", + [ + ("lw t0, 0(a0)", "lw t1, 4(a0)"), + ("lw t0, 0(a0)", "sw t1, 4(a0)"), + ("sw t0, 0(a0)", "lw t1, 4(a0)"), + ("sw t0, 0(a0)", "sw t1, 4(a0)"), + ], +) +def test_memory_order_even_with_different_offsets(first, second): + nodes = graph(first + "\n" + second) + assert nodes[1].predecessors == [(nodes[0], 1)] + assert nodes[1].edge_kinds[0] == {"memory"} + + +def test_fp_status_effects_are_ordered(): + nodes = graph("fadd.s f0, f1, f2\nfdiv.s f3, f4, f5") + assert nodes[1].edge_kinds[0] == {"fp-effects"} + + +def test_exact_heights_and_load_use_wait(): + nodes = graph("lw t0, 0(a0)\nmul t1, t0, t2\nadd t3, t1, t4") + assert [n.priority for n in nodes] == [7, 5, 1] + result = schedule_assembly(EXAMPLE, ScheduleConfig(strict=True)) + assert result.asm_text == EXPECTED + assert result.stats["original_cycles"] == 5 + assert result.stats["final_cycles"] == 4 + assert result.stats["original_stalls"] == 1 + assert result.stats["final_stalls"] == 0 + row = result.stats["regions"][0] + assert row["original_issue_cycles"] == [0, 2, 3, 4] + assert row["candidate_issue_cycles"] == [0, 1, 2, 3] + + +def test_nonpipelined_divider_and_live_out_completion(): + instructions = parse_instructions("div t0, t1, t2\nadd t3, t4, t5\nrem t6, t1, t2") + estimate = estimate_order(instructions) + assert estimate.issue_cycles == (0, 33, 34) + assert estimate.cycles == 67 + assert estimate_order(instructions[:1]).cycles == 33 + + +def test_division_keeps_relative_order_and_blocks_issue_conservatively(): + scheduler = InstructionScheduler() + nodes = scheduler.build_dag( + parse_instructions("div t0, t1, t2\nrem t6, t1, t2\nadd t3, t4, t5") + ) + result = scheduler.schedule(nodes) + assert [i.id for i in result] == [0, 1, 2] + assert estimate_order(result).issue_cycles == (0, 33, 66) + + +def test_custom_model_is_shared_and_copied(): + values = {"lw": 4} + model = ScheduleModel(overrides=values) + values["lw"] = 1 + result = schedule_assembly(EXAMPLE, ScheduleConfig(model=model, strict=True)) + assert result.stats["original_cycles"] == 7 + assert result.stats["final_cycles"] == 6 + assert result.stats["model_overrides"] == {"lw": 4} + with pytest.raises(ValueError): + ScheduleModel(overrides={"add": 0}) + + +@pytest.mark.parametrize( + "newline,last_newline", [("\n", True), ("\n", False), ("\r\n", True)] +) +def test_labels_directives_comments_and_newlines_survive(newline, last_newline): + original = ( + ".text\n.globl f\nf:\nlw t0, 0(a0) # load\n" + "add t1, t0, t2 # consume\naddi t3, t3, 1 # independent\nret\n" + ".size f, .-f\n\n# notes\n.data\nvalue: .word 3\n" + ) + expected = original.replace( + "add t1, t0, t2 # consume\naddi t3, t3, 1 # independent", + "addi t3, t3, 1 # independent\nadd t1, t0, t2 # consume", + ) + if not last_newline: + original, expected = original.rstrip("\n"), expected.rstrip("\n") + assert schedule_assembly( + original.replace("\n", newline) + ).asm_text == expected.replace("\n", newline) + + +@pytest.mark.parametrize( + "separator", ["\n", "# comment\n", ".align 2\n", "label:\n", "1:\n"] +) +def test_no_crossing_non_instruction_lines(separator): + original = "lw t0, 0(a0)\nadd t1, t0, t2\n" + separator + "addi t3, t3, 1\n" + assert schedule_assembly(original).asm_text == original + + +def test_inline_label_and_terminator_bundle_stay_fixed(): + source = "entry: lw t0, 0(a0)\n" + EXAMPLE.replace( + "ret\n", "beq t1, t2, target\nj other\n" + ) + result = schedule_assembly(source, ScheduleConfig(strict=True)) + assert result.asm_text.startswith("entry: lw t0, 0(a0)\n") + assert result.asm_text.endswith("beq t1, t2, target\nj other\n") + assert result.changed + + +@pytest.mark.parametrize( + "source", ["", "# notes", "ret\n", "1:\n2:\n", ".data\n" + EXAMPLE] +) +def test_empty_short_and_non_code_inputs(source): + assert schedule_assembly(source, ScheduleConfig(strict=True)).asm_text == source + + +@pytest.mark.parametrize( + "unsafe", + [ + ".macro f\n.endm", + '.include "extra.s"', + "j 8", + "beq t0, t1, 8", + "add t0, t1, t2; ret", + "lw t0, .-label(a0)", + ], +) +def test_unsafe_assembly_layout_preserves_entire_input(unsafe): + source = EXAMPLE + unsafe + "\n" + result = schedule_assembly(source, ScheduleConfig(strict=True)) + assert result.asm_text == source + assert "entire input preserved" in result.diagnostics[0]["reason"] + + +def test_low_level_api_cannot_silently_drop_boundaries(): + with pytest.raises(ScheduleError, match="Multiple regions"): + graph("lw t0, 0(a0)\nlabel:\nadd t1, t0, t2") + with pytest.raises(ValueError): + machine_instrs_from_scheduled([SchedInst(0, "made_up")]) + + +def test_verifier_checks_reads_without_relying_on_graph(): + original = parse_instructions("add t1, t0, t2\nmul t0, t3, t4") + with pytest.raises(ScheduleError, match="Register"): + verify_schedule(original, list(reversed(original))) + with pytest.raises(ScheduleError, match="identities"): + verify_schedule(original, [original[0], original[0]]) + + +def test_failure_restores_whole_region_and_strict_mode_raises(monkeypatch): + def broken(self, dag): + return [n.inst for n in dag[:-1]] + + monkeypatch.setattr(InstructionScheduler, "schedule", broken) + result = schedule_assembly(EXAMPLE) + assert result.asm_text == EXAMPLE + assert result.stats["regions"][0]["status"] == "restored" + assert result.diagnostics[0]["severity"] == "warning" + with pytest.raises(ScheduleError, match="Line 1"): + schedule_assembly(EXAMPLE, ScheduleConfig(strict=True)) + + +def test_cycles_in_graph_never_return_partial_output(): + scheduler = InstructionScheduler() + nodes = graph("li t0, 1\nli t1, 2") + nodes[0].predecessors.append((nodes[1], 1)) + nodes[1].successors.append((nodes[0], 1)) + nodes[1].predecessors.append((nodes[0], 1)) + nodes[0].successors.append((nodes[1], 1)) + with pytest.raises(ScheduleError, match="cycle"): + scheduler.schedule(nodes) + + +def test_equal_or_worse_candidates_are_not_applied(monkeypatch): + def reverse(self, dag): + return [n.inst for n in reversed(dag)] + + monkeypatch.setattr(InstructionScheduler, "schedule", reverse) + for source in ("li t0, 1\nli t1, 2\n", "mul t0, t1, t2\nli t3, 3\n"): + result = schedule_assembly(source, ScheduleConfig(strict=True)) + assert result.asm_text == source + assert result.stats["regions"][0]["status"] == "no_improvement" + assert result.stats["saved_cycles"] == 0 + + +def test_large_region_skip_and_iterative_dependency_chain(): + source = "addi t0, t0, 1\n" * 1100 + result = schedule_assembly(source) + assert result.asm_text == source + assert result.stats["skipped_regions"] == 1 + result = schedule_assembly( + source, ScheduleConfig(strict=True, max_region_size=1200) + ) + assert result.stats["modeled_instructions"] == 1100 + + +def test_determinism_and_idempotence_on_legal_physical_registers(): + rng = random.Random(18) + for _ in range(50): + source = EXAMPLE.replace("ret\n", "") + for _ in range(30): + dst, a, b = (rng.choice(["t0", "t1", "t2", "t3", "t4"]) for _ in range(3)) + source += f"{rng.choice(['add', 'sub', 'mul'])} {dst}, {a}, {b}\n" + first = schedule_assembly(source, ScheduleConfig(strict=True)) + second = schedule_assembly(source, ScheduleConfig(strict=True)) + assert first.asm_text == second.asm_text + assert first.stats["regions"] == second.stats["regions"] + assert schedule_assembly(first.asm_text).asm_text == first.asm_text