diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d19f288..42059a0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,18 +11,29 @@ env: permissions: contents: read - pages: write - id-token: write jobs: # ═════════════════════════════════════════════════════════════════════════ # 课题功能测试:所有14个模块的单元测试 + 集成测试 # ═════════════════════════════════════════════════════════════════════════ test: - runs-on: self-hosted + runs-on: ${{ github.event_name == 'pull_request' && 'ubuntu-latest' || 'self-hosted' }} timeout-minutes: 20 steps: + - name: Checkout pull request on isolated runner + if: github.event_name == 'pull_request' + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false + + - name: Set up Python for pull request + if: github.event_name == 'pull_request' + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + with: + python-version: "3.12" + - name: Sync from local mirror (skip unstable GitHub checkout) + if: github.event_name != 'pull_request' run: | echo "=== CI Sync ===" retry() { @@ -46,12 +57,6 @@ jobs: cd "$WORKSPACE" echo "Fetching origin main..." retry "git fetch origin main" git fetch origin main || exit 1 - if [ "$GITHUB_EVENT_NAME" = "pull_request" ]; then - PR_NUMBER="${GITHUB_REF#refs/pull/}" - PR_NUMBER="${PR_NUMBER%/merge}" - echo "Fetching PR #${PR_NUMBER} merge ref..." - retry "git fetch PR #${PR_NUMBER}" git fetch origin "refs/pull/${PR_NUMBER}/merge" || exit 1 - fi echo "Checking out $GITHUB_SHA..." if git -c advice.detachedHead=false checkout -f "$GITHUB_SHA"; then echo "::notice::Checkout successful: $(git log -1 --format='%h %ai %s')" @@ -64,6 +69,7 @@ jobs: run: | python3.12 -m pip install --upgrade pip python3.12 -m pip install -e ".[all]" + python3.12 -m pip install "pytest>=7,<10" - name: Run all topic tests run: | @@ -72,6 +78,14 @@ jobs: --junit-xml=benchmark_reports/test_results.xml \ --ignore=tests/test_simulator.py + - name: Run PR #35 constant-merge regressions + run: | + python3.12 -m pytest \ + tests/test_const_merge.py \ + tests/test_backend.py::TestConstMergeIntegration \ + tests/test_simulator.py::TestStubProfiledMachine \ + -v --tb=short + - name: Generate test visualization page if: github.ref == 'refs/heads/main' run: | @@ -81,7 +95,7 @@ jobs: - name: Upload test reports if: github.ref == 'refs/heads/main' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: test-reports path: | @@ -93,10 +107,23 @@ jobs: # 模型性能测试:ONNX模型管线 + DSL用例 + CNN RISC-V编译 # ═════════════════════════════════════════════════════════════════════════ benchmark: - runs-on: self-hosted + runs-on: ${{ github.event_name == 'pull_request' && 'ubuntu-latest' || 'self-hosted' }} timeout-minutes: 30 steps: + - name: Checkout pull request on isolated runner + if: github.event_name == 'pull_request' + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false + + - name: Set up Python for pull request + if: github.event_name == 'pull_request' + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + with: + python-version: "3.12" + - name: Sync from local mirror (skip unstable GitHub checkout) + if: github.event_name != 'pull_request' run: | echo "=== CI Sync ===" retry() { @@ -120,12 +147,6 @@ jobs: cd "$WORKSPACE" echo "Fetching origin main..." retry "git fetch origin main" git fetch origin main || exit 1 - if [ "$GITHUB_EVENT_NAME" = "pull_request" ]; then - PR_NUMBER="${GITHUB_REF#refs/pull/}" - PR_NUMBER="${PR_NUMBER%/merge}" - echo "Fetching PR #${PR_NUMBER} merge ref..." - retry "git fetch PR #${PR_NUMBER}" git fetch origin "refs/pull/${PR_NUMBER}/merge" || exit 1 - fi echo "Checking out $GITHUB_SHA..." if git -c advice.detachedHead=false checkout -f "$GITHUB_SHA"; then echo "::notice::Checkout successful: $(git log -1 --format='%h %ai %s')" @@ -138,7 +159,7 @@ jobs: run: | python3.12 -m pip install --upgrade pip python3.12 -m pip install -e ".[all]" - python3.12 -m pip install markdown + python3.12 -m pip install markdown "pytest>=7,<10" # ── 3.1 ONNX 模型管线基准测试 ────────────────────────────────────── - name: ONNX model pipeline benchmarks @@ -208,7 +229,7 @@ jobs: # ── 上报 ────────────────────────────────────────────────────────── - name: Upload benchmark reports - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: benchmark-reports path: benchmark_reports/ @@ -253,7 +274,7 @@ jobs: # ── 上传 Pages 产物 ────────────────────────────────────────────── - name: Upload Pages artifact if: github.ref == 'refs/heads/main' - uses: actions/upload-pages-artifact@v3 + uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3 with: path: benchmark_reports/ @@ -284,4 +305,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v4 + uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4 diff --git a/benchmarks/bench_const_merge.py b/benchmarks/bench_const_merge.py index 21d837b..ddc37ee 100644 --- a/benchmarks/bench_const_merge.py +++ b/benchmarks/bench_const_merge.py @@ -12,56 +12,91 @@ from __future__ import annotations import argparse +import os import statistics +import sys import time -from typing import Optional -from scratchv.backend.const_merge import merge_constants +BENCH_DIR = os.path.dirname(__file__) +PROJ_DIR = os.path.dirname(BENCH_DIR) +sys.path.insert(0, PROJ_DIR) +from scratchv.backend._asm_parser import parse_asm +from scratchv.backend.const_merge import merge_constants_detailed -def _gen_synthetic_asm(num_instrs: int, seed: int = 42, - lui_ratio: float = 0.3) -> str: - """Generate synthetic assembly with lui+addi patterns. + +def _gen_synthetic_asm( + num_instructions: int, + seed: int = 42, + pair_density: float = 0.3, + redundant_lui_density: float = 0.1, +) -> str: + """Generate controlled synthetic assembly covering both optimization rules. Parameters ---------- - num_instrs: + num_instructions: Target number of instructions. seed: Random seed for reproducibility. - lui_ratio: + pair_density: Fraction of instructions that form lui+addi pairs. + redundant_lui_density: + Fraction of generated groups that contain a redundant LUI pattern. """ import random - random.seed(seed) + + if num_instructions < 0: + raise ValueError("num_instructions must be non-negative") + if not 0.0 <= pair_density <= 1.0: + raise ValueError("pair_density must be between 0 and 1") + if not 0.0 <= redundant_lui_density <= 1.0: + raise ValueError("redundant_lui_density must be between 0 and 1") + if pair_density + redundant_lui_density > 1.0: + raise ValueError( + "pair_density + redundant_lui_density must not exceed 1", + ) + + rng = random.Random(seed) lines = [".text", "synthetic_func:"] i = 0 - while i < num_instrs: - use_lui = random.random() < lui_ratio - - if use_lui and i + 1 < num_instrs: + while i < num_instructions: + choice = rng.random() + + if choice < redundant_lui_density and i + 2 < num_instructions: + regs = ["t0", "t1", "t2", "s0", "s1", "a0", "a1"] + reg = rng.choice(regs) + imm_hi = rng.choice([0x10000, 0x20000, 0x12345]) + lines.append(f" lui {reg}, {hex(imm_hi)}") + lines.append(" add a4, a5, a6") + lines.append(f" lui {reg}, {hex(imm_hi)}") + i += 3 + elif ( + choice < redundant_lui_density + pair_density + and i + 1 < num_instructions + ): regs = ["t0", "t1", "t2", "s0", "s1", "a0", "a1", "a2", "a3"] - r = random.choice(regs) - imm_hi = random.choice([0x10000, 0x20000, 0x12345, 0xABCDE, 0xFFFFF]) - imm_lo = random.choice([0x000, 0x100, 0x678, 0xFFF, 0x800]) + r = rng.choice(regs) + imm_hi = rng.choice([0x10000, 0x20000, 0x12345, 0xABCDE, 0xFFFFF]) + imm_lo = rng.choice([0x000, 0x100, 0x678, 0xFFF, 0x800]) lines.append(f" lui {r}, {hex(imm_hi)}") lines.append(f" addi {r}, {r}, {hex(imm_lo)}") i += 2 else: - op = random.choice(["add", "sub", "lw", "sw", "mv", "mul", "xor", - "li", "addi", "beq", "j", "ret"]) + op = rng.choice(["add", "sub", "lw", "sw", "mv", "mul", "xor", + "li", "addi", "beq", "j", "ret"]) regs = ["t0", "t1", "t2", "t3", "t4", "s0", "s1", "a0", "a1", "a2", "a3", "sp", "ra"] - r1 = random.choice(regs) - r2 = random.choice(regs) - r3 = random.choice(regs) + r1 = rng.choice(regs) + r2 = rng.choice(regs) + r3 = rng.choice(regs) if op == "li": - lines.append(f" {op} {r1}, {random.randint(0, 4096)}") + lines.append(f" {op} {r1}, {rng.randint(0, 4096)}") elif op == "addi": - lines.append(f" {op} {r1}, {r2}, {random.randint(-2048, 2047)}") + lines.append(f" {op} {r1}, {r2}, {rng.randint(-2048, 2047)}") elif op in ("lw", "sw"): - lines.append(f" {op} {r1}, {random.randint(0, 16)}(sp)") + lines.append(f" {op} {r1}, {rng.randint(0, 16)}(sp)") elif op in ("beq", "bne", "blt", "bge"): lines.append(f" {op} {r1}, {r2}, label_{i}") elif op == "j": @@ -78,24 +113,39 @@ def _gen_synthetic_asm(num_instrs: int, seed: int = 42, def bench_merge(asm_text: str, repeats: int = 50) -> dict: """Benchmark the constant merge optimizer.""" + if repeats < 1: + raise ValueError("repeats must be at least 1") times = [] results = [] for _ in range(repeats): t0 = time.perf_counter() - result, changes = merge_constants(asm_text) + result, stats = merge_constants_detailed(asm_text) t1 = time.perf_counter() times.append(t1 - t0) - results.append((result, changes)) - - changes_list = [r[1] for r in results] - input_lines = asm_text.count("\n") - output_lines = results[0][0].count("\n") if results else 0 + results.append((result, stats)) + + changes_list = [r[1].total_changes for r in results] + first_stats = results[0][1] + parsed_input = parse_asm(asm_text) + parsed_output = parse_asm(results[0][0]) + input_instructions = sum( + line.opcode is not None and not line.is_directive + for line in parsed_input + ) + output_instructions = sum( + line.opcode is not None and not line.is_directive + for line in parsed_output + ) return { - "input_lines": input_lines, - "output_lines": output_lines, - "line_reduction": input_lines - output_lines, + "benchmark_type": "synthetic", + "input_instructions": input_instructions, + "output_instructions": output_instructions, + "instruction_reduction": input_instructions - output_instructions, + "candidate_pairs": first_stats.candidate_pairs, + "merged_pairs": first_stats.merged_pairs, + "redundant_lui_removed": first_stats.redundant_lui_removed, "changes_mean": statistics.mean(changes_list), "changes_stdev": statistics.stdev(changes_list) if len(changes_list) > 1 else 0, "repeats": repeats, @@ -111,35 +161,50 @@ def main(): parser = argparse.ArgumentParser(description="Constant Merge Benchmark") parser.add_argument("--repeats", type=int, default=50, help="Number of repeat measurements") + parser.add_argument("--seed", type=int, default=42) + parser.add_argument("--pair-density", type=float, default=0.3) + parser.add_argument("--redundant-lui-density", type=float, default=0.1) args = parser.parse_args() sizes = [100, 500, 1000, 2000, 5000] print("=" * 80) print("RISC-V Constant Load Merge Optimizer Benchmark") + print("benchmark_type=synthetic") print("=" * 80) print(f"\n{'Size':>8} {'Mean(ms)':>10} {'Stdev(ms)':>10} " - f"{'Changes':>8} {'InpLines':>10} {'OutLines':>10} {'Reduc':>8}") + f"{'Pairs':>8} {'RedLUI':>8} {'InpInst':>10} {'OutInst':>10}") print("-" * 80) for size in sizes: - asm = _gen_synthetic_asm(size, lui_ratio=0.3) + asm = _gen_synthetic_asm( + size, + seed=args.seed, + pair_density=args.pair_density, + redundant_lui_density=args.redundant_lui_density, + ) stats = bench_merge(asm, repeats=args.repeats) print(f"{size:>8} {stats['mean_s'] * 1000:>10.3f} " f"{stats['stdev_s'] * 1000:>10.3f} " - f"{stats['changes_mean']:>8.1f} " - f"{stats['input_lines']:>10} {stats['output_lines']:>10} " - f"{stats['line_reduction']:>8}") + f"{stats['merged_pairs']:>8} " + f"{stats['redundant_lui_removed']:>8} " + f"{stats['input_instructions']:>10} " + f"{stats['output_instructions']:>10}") # Test different lui densities print(f"\nLUI Density Impact (2000 instructions):") print("-" * 60) for ratio in [0.0, 0.1, 0.3, 0.5]: - asm = _gen_synthetic_asm(2000, lui_ratio=ratio) + asm = _gen_synthetic_asm( + 2000, + seed=args.seed, + pair_density=ratio, + redundant_lui_density=args.redundant_lui_density, + ) stats = bench_merge(asm, repeats=args.repeats) print(f" ratio={ratio:.1f} {stats['mean_s'] * 1000:.3f} ms " f"changes: {stats['changes_mean']:.1f} " - f"reduction: {stats['line_reduction']}") + f"reduction: {stats['instruction_reduction']}") if __name__ == "__main__": diff --git a/benchmarks/run_benchmark.py b/benchmarks/run_benchmark.py index f473214..8d5bad6 100644 --- a/benchmarks/run_benchmark.py +++ b/benchmarks/run_benchmark.py @@ -36,6 +36,7 @@ from scratchv.frontend.dsl_parser import DSLParser from scratchv.ir.builder import IRBuilder from scratchv.ir.types import Program +from scratchv.backend._asm_parser import ParsedAsmLine, parse_asm # --------------------------------------------------------------------------- @@ -55,6 +56,18 @@ class BenchResult: ir_opt_inst_count: int = 0 codegen_time_s: float = 0.0 asm_line_count: int = 0 + lui_count_before: int = 0 + candidate_pairs: int = 0 + merged_pairs: int = 0 + redundant_lui_removed: int = 0 + asm_instructions_before: int = 0 + asm_instructions_after: int = 0 + machine_instructions_before: Optional[int] = None + machine_instructions_after: Optional[int] = None + code_size_before: Optional[int] = None + code_size_after: Optional[int] = None + pass_time_ms: float = 0.0 + output_equal: Optional[bool] = None total_time_s: float = 0.0 verified: bool = False error: Optional[str] = None @@ -70,6 +83,19 @@ def _count_ir(program: Program) -> tuple[int, int]: return inst, bb +def _count_parsed_asm_instructions(lines: list[ParsedAsmLine]) -> int: + """Count real assembly statements, excluding labels and directives.""" + return sum( + line.opcode is not None and not line.is_directive + for line in lines + ) + + +def _count_asm_instructions(asm_text: str) -> int: + """Parse and count assembly instructions using the shared parser.""" + return _count_parsed_asm_instructions(parse_asm(asm_text)) + + def _parse_onnx(path: str) -> Program: parser = ONNXParser() return parser.parse(path) @@ -173,6 +199,24 @@ def run_benchmark(model_name: str, model_path: str, *, asm_str, result.codegen_time_s = _codegen_llvm(program) else: asm_str, result.codegen_time_s = _codegen_riscv(program) + from scratchv.backend.const_merge import merge_constants_detailed + + # Single-variable A/B: both sides come from this exact codegen + # result, so frontend, optimization and allocation are identical. + parsed_before = parse_asm(asm_str) + result.lui_count_before = sum( + line.opcode == "lui" for line in parsed_before + ) + result.asm_instructions_before = _count_parsed_asm_instructions( + parsed_before, + ) + t0 = time.perf_counter() + asm_after, merge_stats = merge_constants_detailed(asm_str) + result.pass_time_ms = (time.perf_counter() - t0) * 1000 + result.candidate_pairs = merge_stats.candidate_pairs + result.merged_pairs = merge_stats.merged_pairs + result.redundant_lui_removed = merge_stats.redundant_lui_removed + result.asm_instructions_after = _count_asm_instructions(asm_after) result.asm_line_count = len(asm_str.splitlines()) # 4. Verify @@ -199,7 +243,8 @@ def run_all_benchmarks(models: dict[str, str], backend: str = "riscv", print(f"ERROR: {r.error}") else: print(f"done ({r.total_time_s:.3f}s, {r.ir_inst_count} IR inst, " - f"{r.asm_line_count} asm lines)") + f"{r.asm_line_count} asm lines, " + f"{r.merged_pairs + r.redundant_lui_removed} const changes)") results.append(r) return results @@ -238,6 +283,26 @@ def print_summary(results: list[BenchResult]): for r in errors: print(f" - {r.model_name}: {r.error}") + riscv_results = [r for r in results if r.backend == "riscv"] + if riscv_results: + print("\nCONST-MERGE A/B") + print("-" * 90) + print( + f"{'Model':<16} {'LUI':>6} {'Candidates':>10} {'Pairs':>8} " + f"{'RedLUI':>8} {'Asm before→after':>18} {'Pass(ms)':>10}" + ) + for r in riscv_results: + asm_counts = ( + f"{r.asm_instructions_before}→{r.asm_instructions_after}" + ) + print( + f"{r.model_name:<16} {r.lui_count_before:>6} " + f"{r.candidate_pairs:>10} {r.merged_pairs:>8} " + f"{r.redundant_lui_removed:>8} {asm_counts:>18} " + f"{r.pass_time_ms:>10.3f}" + ) + print("Machine instructions/code size/output equality: N/A without toolchain") + def save_results(results: list[BenchResult], output_path: str): data = [asdict(r) for r in results] diff --git a/benchmarks/test_benchmark.py b/benchmarks/test_benchmark.py index 74e08c2..c6091de 100644 --- a/benchmarks/test_benchmark.py +++ b/benchmarks/test_benchmark.py @@ -12,6 +12,7 @@ from __future__ import annotations import os +import json import sys import time @@ -22,7 +23,9 @@ sys.path.insert(0, PROJ_DIR) from benchmarks.generate_models import ensure_all_models -from benchmarks.run_benchmark import run_benchmark +from benchmarks.bench_const_merge import _gen_synthetic_asm, bench_merge +from benchmarks.run_benchmark import BenchResult, print_summary, run_benchmark, save_results +from benchmarks.run_benchmark import _count_asm_instructions # --------------------------------------------------------------------------- @@ -38,6 +41,91 @@ def benchmark_models() -> dict[str, str]: BACKEND_PARAMS = ["riscv"] +def test_effective_asm_instruction_count(): + asm = ".text\nmain:\n li t0, 1\n# comment\n add t1, t0, t0\n" + assert _count_asm_instructions(asm) == 2 + + +def test_synthetic_benchmark_covers_both_rules(): + asm = _gen_synthetic_asm( + 200, + seed=42, + pair_density=0.3, + redundant_lui_density=0.2, + ) + stats = bench_merge(asm, repeats=2) + assert stats["benchmark_type"] == "synthetic" + assert stats["merged_pairs"] > 0 + assert stats["redundant_lui_removed"] > 0 + assert stats["instruction_reduction"] == ( + stats["merged_pairs"] + stats["redundant_lui_removed"] + ) + assert stats["input_instructions"] == 200 + + +@pytest.mark.parametrize( + "pair_density,redundant_density", + [(-0.1, 0.1), (0.1, -0.1), (1.1, 0.0), (0.6, 0.5)], +) +def test_synthetic_density_validation(pair_density, redundant_density): + with pytest.raises(ValueError): + _gen_synthetic_asm( + 10, + pair_density=pair_density, + redundant_lui_density=redundant_density, + ) + + +def test_synthetic_seed_is_reproducible(): + kwargs = { + "num_instructions": 100, + "seed": 7, + "pair_density": 0.2, + "redundant_lui_density": 0.1, + } + assert _gen_synthetic_asm(**kwargs) == _gen_synthetic_asm(**kwargs) + + +def test_synthetic_repeats_validation(): + with pytest.raises(ValueError, match="repeats"): + bench_merge(" nop\n", repeats=0) + + +def _zero_hit_result() -> BenchResult: + return BenchResult( + model_name="zero_hit", + model_path="model.onnx", + backend="riscv", + optimize_level="all", + parse_time_s=0.0, + ir_inst_count=1, + ir_bb_count=1, + asm_instructions_before=3, + asm_instructions_after=3, + ) + + +def test_summary_keeps_zero_hit_case(capsys): + print_summary([_zero_hit_result()]) + output = capsys.readouterr().out + assert "CONST-MERGE A/B" in output + assert "zero_hit" in output + assert "3→3" in output + assert "N/A without toolchain" in output + + +def test_json_keeps_zero_and_na_fields(tmp_path, capsys): + output = tmp_path / "results.json" + save_results([_zero_hit_result()], str(output)) + capsys.readouterr() + data = json.loads(output.read_text())[0] + assert data["candidate_pairs"] == 0 + assert data["merged_pairs"] == 0 + assert data["redundant_lui_removed"] == 0 + assert data["machine_instructions_before"] is None + assert data["output_equal"] is None + + def _model_id(name: str) -> str: return name @@ -151,6 +239,17 @@ def test_perf_pipeline(model_name: str, benchmark_models: dict[str, str]): assert result.error is None, f"Benchmark failed: {result.error}" assert result.ir_inst_count > 0 + assert isinstance(result.asm_instructions_before, int) + assert isinstance(result.asm_instructions_after, int) + reduction = ( + result.asm_instructions_before - result.asm_instructions_after + ) + tracked_changes = result.merged_pairs + result.redundant_lui_removed + assert reduction == tracked_changes, ( + f"instruction reduction {reduction} != tracked changes " + f"{tracked_changes}" + ) + assert result.pass_time_ms >= 0 print(f"\n {model_name}:") print(f" parse: {result.parse_time_s:.4f}s") diff --git "a/docs/\350\257\276\351\242\23014-\345\270\270\351\207\217\345\212\240\350\275\275\345\220\210\345\271\266\344\274\230\345\214\226-\345\274\200\345\217\221\346\226\207\346\241\243\345\210\235\347\250\277.md" "b/docs/\350\257\276\351\242\23014-\345\270\270\351\207\217\345\212\240\350\275\275\345\220\210\345\271\266\344\274\230\345\214\226-\345\274\200\345\217\221\346\226\207\346\241\243\345\210\235\347\250\277.md" new file mode 100644 index 0000000..30e89ed --- /dev/null +++ "b/docs/\350\257\276\351\242\23014-\345\270\270\351\207\217\345\212\240\350\275\275\345\220\210\345\271\266\344\274\230\345\214\226-\345\274\200\345\217\221\346\226\207\346\241\243\345\210\235\347\250\277.md" @@ -0,0 +1,586 @@ +# ScratchV 课题 14:常量加载合并优化开发文档 + +> **文档版本**:v0.4(实现、AI review 与本地验证状态回填) +> **创建日期**:2026-07-28 +> **更新日期**:2026-08-16 +> **作者**:[yuki] +> **关联 Issue**:[#待补充] +> **涉及模块**:`scratchv/backend/`、`scratchv/compiler.py`、`scratchv/main.py`、`tests/`、`docs/` + +--- + +## 0. 开发前说明 + +ScratchV 主分支已经存在课题 14 的参考模块、测试和命令行集成。本开发计划将任务定义为: + +> 对现有常量加载合并优化进行行为梳理、安全性加固、共享解析器重构、边界测试补全和集成验证。 + +### 0.1 当前实现状态 + +已完成共享解析器迁移、寄存器别名规范化、安全的 RV32 `lui+addi` 合并、基本块内冗余 `lui` 删除、固定点迭代、分类统计、独立 CLI 与 CompilerDriver 集成、真实 case A/B 和 synthetic benchmark。旧接口 `merge_constants(...)->(str, int)` 保持兼容。 + +真实 case 抽查为零 `lui`、零候选和零转换,符合 `instruction_select.py` 直接生成 `MachineOp.LI` 的现状。当前环境没有 GNU RISC-V assembler 或 Spike,机器指令数、代码大小和执行等价字段按设计保留为 `N/A`。 + +本轮维护修复了 TinyFive stub 在 `_m` 为 `None` 时误用真实机器方法的问题,并补全汇编计数、指令上限、寄存器索引及有符号 32 位内存读写测试。AI review 进一步修复了未修改汇编文本被规范化重写、数字/美元标签识别、未知行边界、大小写寄存器定义识别、stub 伪操作计数和 `x0` 写入等问题。当前本地全量回归为 439 项通过、4 项按环境条件跳过、零失败。 + + +--- + +## 1. 功能概述与目标 + +### 1.1 背景与动机 + +- **现状问题**:RISC-V 加载一般 32 位常量常出现 `lui + addi` 序列;重复加载相同高 20 位还可能产生冗余 `lui`。`li` 是汇编器伪指令,不是 RV32I 的单条真实机器指令。现有参考实现具备基本功能,但仍需要补充控制流安全、寄存器别名、共享解析器和更严格的测试。 +- **应用场景**:处理 ScratchV 后端生成的汇编、外部 RISC-V 汇编文件、课程测试 fixture,以及汇编级窥孔优化和统计流程。 +- **现实限制(已按 commit `109a6a2` 核实)**:`instruction_select.py` 的常量、循环初值等路径直接生成 `MachineOp.LI`。因此现有 ONNX/DSL case 可能不产生 `lui+addi`,主流水线命中率可能为 0。零命中是需要记录的实验结果,不是需要通过改造真实输入掩盖的问题。 + +### 1.2 功能描述 + +- **一句话定义**:在汇编生成后识别安全的常量加载模式,将 `lui+addi` 规范化为 `li`,并删除同一基本块内可证明冗余的 `lui`。 +- **核心价值**:提高汇编可读性,消除真实冗余指令,建立可复用的汇编级优化开发流程。 + +### 1.3 目标与非目标 + +| 类型 | 内容 | +|---|---| +| ✅ 包含范围 | RV32 数值立即数;12 位符号扩展;相邻 `lui+addi`;基本块内冗余 `lui`;寄存器别名;迭代扫描;统计;独立 CLI;主编译器开关;单元与集成测试 | +| ❌ 不包含范围 | RV64 完整常量构造;符号重定位;跨基本块数据流;跨函数复用;IR 级常量折叠;完整汇编器实现 | + +--- + +## 2. 设计与规格说明 + +### 2.1 外部接口 + +#### Python API + +保持: + +```python +from scratchv.backend.const_merge import merge_constants + +optimized_asm, changes = merge_constants(asm_text) +``` + +已新增: + +```python +optimized_asm, stats = merge_constants_detailed(asm_text) +``` + +#### 独立 CLI + +```bash +python -m scratchv.backend.const_merge input.s -o output.s -v +``` + +#### ScratchV 主 CLI + +```bash +scratchv input.dsl -o output.s --const-merge +``` + +注意:归档课题写的是 `--merge-constants`,但当前主分支真实参数为 `--const-merge`。本开发默认服从当前代码;是否增加旧名称别名由评审决定。 + +### 2.2 内部设计 + +#### 数据结构 + +- 复用 `scratchv/backend/_asm_parser.py` 中的 `ParsedAsmLine`; +- 新增或内部使用 `ConstantMergeStats`; +- 使用 `dict[str, int]` 跟踪基本块内各物理寄存器的最后一次 `lui` 值; +- 使用 ABI 名到 `xN` 的映射做寄存器规范化。 + +#### 核心处理流程 + +```text +读取汇编文本 + -> parse_asm + -> 固定点循环 + -> 删除基本块内冗余 lui + -> 合并安全的 lui+addi + -> lines_to_asm + -> 返回输出与统计 +``` + +#### 状态清空条件 + +遇到以下情况清空或失效化寄存器状态: + +- 标签; +- 条件分支; +- 无条件跳转; +- `call`、`jal`、`jalr`; +- `ret`、`jr`; +- 对已跟踪寄存器产生定义的普通指令使该寄存器状态失效;这不妨碍规则 A 对相邻 `lui+addi` 进行整体匹配; +- 无法可靠分析的未知指令一律清空全部状态。 + +### 2.3 模块间交互 + +- **上游**:接收 `AsmEmitter` 或线性扫描寄存器分配器生成的汇编文本,也可直接接收用户输入 `.s`; +- **下游**:输出给调度器、汇编美化器、指令计数器和最终文件写入; +- **对 IR 无影响**:不修改 AST、IR 或寄存器分配结果; +- **Pass 顺序**:位于汇编窥孔之后、调度器之前,使窥孔优化暴露的模式可以被合并,并避免调度器打散待匹配的相邻序列。 + +--- + +## 3. 开发环境与基线 + +### 3.1 Ubuntu 24.04 环境初始化 + +```bash +git clone https://github.com/ScratchV-Compiler/ScratchV.git +cd ScratchV + +python3 -m venv .venv +source .venv/bin/activate +python -m pip install --upgrade pip +pip install -e . +``` + +可选汇编验证工具: + +```bash +sudo apt update +sudo apt install binutils-riscv64-linux-gnu +``` + +### 3.2 建立功能分支 + +```bash +git switch main +git pull +git switch -c feat/topic14-const-merge +``` + +### 3.3 运行基线测试 + +```bash +pytest tests/test_const_merge.py -v +pytest tests/ -q +``` + +记录: + +- 当前 commit hash; +- Python 版本; +- 测试总数、通过数、失败数; +- 当前 `const_merge` 测试输出; +- 一份未修改前的示例汇编结果。 + +```bash +git rev-parse HEAD +python --version +``` + +--- + +## 4. 涉及文件清单 + +| 文件路径 | 修改类型 | 修改内容概述 | +|---|---|---| +| `scratchv/backend/const_merge.py` | 重点修改 | 复用共享解析器;实现安全匹配、寄存器规范化、块边界处理、固定点迭代和详细统计 | +| `scratchv/backend/_asm_parser.py` | 复用/小改 | 必要时补充公开的寄存器定义/使用判断;避免在 const-merge 中再写一套解析器 | +| `scratchv/compiler.py` | 检查/小改 | 确认 `const_merge` 在 post-codegen 中的顺序和统计输出;必要时接入详细统计 | +| `scratchv/main.py` | 检查/小改 | 确认 `--const-merge` 参数及配置传递;可选增加旧参数别名 | +| `tests/test_const_merge.py` | 重点修改 | 补充边界、安全、别名、迭代、幂等性和 CLI 测试 | +| `tests/test_backend.py` | 修改 | 增加 CompilerConfig/CompilerDriver 集成验证 | +| `tests/fixtures/const_merge/*.s` | 新增 | 存放可读的汇编输入、预期输出和控制流反例 | +| `docs/topics/14-常量加载合并优化.md` 或对应文档源 | 修改 | 更新算法原理、限制、命令和测试结果 | + +实际提交前先用以下命令确认路径: + +```bash +find scratchv/backend -maxdepth 1 -type f | sort +grep -R "const_merge" -n scratchv tests docs | head -100 +``` + +--- + +## 5. 分步实现计划 + +### 步骤 0:确认课题边界 + +**任务**:向维护者确认 RV32/RV64、跨基本块、API 和 CLI 命名。 +**产出**:评审结论写入设计文档“工作假设”章节。 +**验证**:所有待评审问题有明确答案或标记为非目标。 + +### 步骤 1:补 characterization tests + +**任务**:先把当前实现行为固定下来,不立即重构。 +**产出**:现有正向样例、无匹配样例、CLI 可导入测试。 +**验证**:修改前新增测试通过,或明确暴露当前 bug。 + +### 步骤 2:迁移到共享汇编解析器 + +**任务**:用 `parse_asm`、`lines_to_asm` 和 `classify_def_use` 替代重复解析逻辑。 +**产出**:`const_merge.py` 不再维护完整的独立 `AsmInst` 解析器,或仅保留兼容别名。 +**验证**:标签、注释、空行、内存操作数的解析测试通过。 + +### 步骤 3:实现寄存器规范化 + +**任务**:建立 ABI 名到 `xN` 的映射。 +**产出**:`canonical_reg("t0") == "x5"`。 +**验证**:别名混用测试通过。 + +### 步骤 4:实现 `lui+addi` 安全合并 + +**任务**: + +- 检查操作码和操作数数量; +- 检查 `rd == addi.rd == addi.rs1`; +- 只接受纯数值立即数; +- 处理 12 位符号扩展; +- 按 RV32 截断; +- 不跨标签; +- 保留注释。 + +**产出**:`merge_lui_addi_once()`。 +**验证**:正数、负数和边界立即数测试通过。 + +### 步骤 5:实现块内冗余 `lui` 消除 + +**任务**: + +- 维护 `lui_state`; +- 遇到定义寄存器的指令使对应状态失效; +- 遇到标签、分支、跳转、调用和返回清空状态; +- 使用寄存器规范化; +- 未知指令保守处理。 + +**产出**:`remove_redundant_lui_once()`。 +**验证**:安全删除和控制流反例测试通过。 + +### 步骤 6:实现固定点迭代与统计 + +**任务**:迭代运行两个规则直到无变化,记录分类统计。 +**产出**:`ConstantMergeStats`、迭代次数和兼容包装函数。 +**验证**:需要两轮才能完成的样例通过;第二次运行零变化。 + +### 步骤 7:集成主编译器 + +**任务**:检查: + +```text +CompilerConfig.const_merge +main.py --const-merge +CompilerDriver._run_asm_passes +``` + +**产出**:主 CLI 和 Python 配置均可启用优化。 +**验证**:集成测试确认参数被传递并调用优化器。 + +### 步骤 8:工具链语义验证 + +**任务**:把优化前后汇编分别交给 GNU assembler。 +**产出**:对象文件和反汇编对比记录。 +**验证**:两者能汇编,并在测试程序中产生相同返回值/寄存器值。 + +示例: + +```bash +riscv64-linux-gnu-as -march=rv32im before.s -o before.o +riscv64-linux-gnu-as -march=rv32im after.s -o after.o +riscv64-linux-gnu-objdump -d -M no-aliases before.o > before.dump +riscv64-linux-gnu-objdump -d -M no-aliases after.o > after.dump +diff -u before.dump after.dump +``` + +`li` 可能重新展开为 `lui+addi`,因此反汇编差异不应只看源文件行数。 + +### 步骤 9:改造 Benchmark 并建立真实 case A/B + +**历史基线(commit `109a6a2`,以下缺口现已完成整改)**: + +- `benchmarks/bench_runner.py` 测 DSL/IR 执行,不进入 RISC-V 汇编 post-pass,不能证明 const-merge 有效; +- `benchmarks/run_benchmark.py` 能生成 RISC-V 汇编,但当前不调用 `merge_constants`,并且只记录 `asm_line_count`; +- `benchmarks/bench_const_merge.py` 是 synthetic microbenchmark,当前只生成 `lui+addi` 模式、只返回总变化数,并用换行数表示规模与收益。 + +**任务 A:真实 case 命中率。** 在 `run_benchmark.py` 的 RISC-V 路径中,对同一份原始汇编做单变量 A/B: + +```python +asm_before = asm_str +t0 = time.perf_counter() +asm_after, stats = merge_constants_detailed(asm_before) +pass_time_ms = (time.perf_counter() - t0) * 1000 +``` + +不得通过分别执行两次完整编译来取得 before/after,否则会把前端、优化、指令选择和寄存器分配差异混入结果。也不得修改现有 ONNX/DSL case 来制造目标模式。 + +每个真实 case 至少输出: + +| 字段 | 说明 | +|---|---| +| `lui_count_before` | 优化前有效 `lui` 数量 | +| `candidate_pairs` | 同块内操作码和操作数数量匹配、可进入安全检查的 `lui+addi` 结构候选数;即使随后因寄存器或立即数校验失败也保留统计 | +| `merged_pairs` | 规则 A 实际转换数 | +| `redundant_lui_removed` | 规则 B 实际删除数 | +| `asm_instructions_before/after` | 排除标签、伪操作、空行和纯注释后的汇编指令数 | +| `machine_instructions_before/after` | 汇编并反汇编后的真实机器指令数;工具链不可用时记为 `N/A` | +| `code_size_before/after` | `.text` 大小;工具链不可用时记为 `N/A` | +| `pass_time_ms` | 仅 const-merge 的耗时 | +| `output_equal` | 优化前后可执行结果是否一致;无法执行时不得写 `true`,应记为 `N/A` | + +**任务 B:人工 microbenchmark。** 保留 `bench_const_merge.py`,但明确标记为 synthetic,并增加可调规模、目标模式密度和重复 `lui` 密度。输出 `merged_pairs` 与 `redundant_lui_removed`,使用有效指令计数替代 `asm_text.count("\n")`。人工输入的下降量只能说明算法对目标模式有效,不能表述为真实项目端到端收益。 + +**结果判定:** + +- 真实 case 有命中:报告命中 case、两类转换数量、机器指令/代码大小变化及语义验证; +- 真实 case 零命中:明确写出“当前代码生成形式与 const-merge 目标输入不匹配”,同时用 microbenchmark 证明规则正确性和扫描开销; +- 规则 A 仅减少源汇编行数、机器指令不变:按事实分别报告,不宣称运行性能提升; +- 只有规则 B 的删除可直接计入真实机器指令减少,仍需工具链及语义验证支持。 + +**验证**:固定 seed 的 benchmark 可重复;统计满足 `total_changes == merged_pairs + redundant_lui_removed`;有效指令差值与转换分类一致;JSON/表格中保留零值与 `N/A`。 + +### 步骤 10:回归、文档与 PR + +**任务**: + +```bash +pytest tests/test_const_merge.py -v +pytest tests/ -q +make check # 若当前仓库提供 +``` + +**产出**:代码、测试、设计文档、开发文档、使用说明和 PR 描述。 +**验证**:所有验收标准完成。 + +--- + +## 6. 异常处理与边界条件 + +- [x] 空字符串输入返回空结果和零变化; +- [x] 只有注释或标签时不报错; +- [x] 操作数缺失时跳过,不抛出未处理异常; +- [x] `0x7FF`、`0x800`、`0xFFF` 正确符号扩展; +- [x] `0x80000000` 等 32 位边界按 RV32 规范化; +- [x] `-0x1` 可解析; +- [x] `%hi(symbol)`、`%lo(symbol)` 保持原样; +- [x] `t0` 和 `x5` 被视为同一寄存器; +- [x] 非法名称以及浮点、向量寄存器不参与整数常量合并; +- [x] 标签或分支不会导致错误删除; +- [x] `call` 后不复用 caller-saved 寄存器状态; +- [x] 注释、标签、首尾空行和空白行缩进不会静默丢失; +- [x] 发生转换时,未修改的字符串伪操作、数字/美元标签和其他原始汇编文本逐字保留; +- [x] 未知操作码采用保守策略; +- [x] 达到迭代上限时能正常停止; +- [x] 优化结果具有字符串级幂等性。 + +--- + +## 7. 测试与验证方案 + +### 7.1 单元测试矩阵 + +| 编号 | 场景 | 输入关键点 | 预期 | +|---|---|---|---| +| T01 | 基本合并 | `lui t0,0x12345` + `addi t0,t0,0x678` | 一条 `li` | +| T02 | 低位最大正数 | `0x7FF` | +2047 | +| T03 | 低位最小负数 | `0x800` | -2048 | +| T04 | 低位 -1 | `0xFFF` | -1 | +| T05 | 目标不同 | `addi t1,t0,...` | 不合并 | +| T06 | 源不同 | `addi t0,t1,...` | 不合并 | +| T07 | 中间注释 | 注释/空行 | 可按设计合并并保留注释 | +| T08 | 中间标签 | `L1:` | 不合并 | +| T09 | 重定位 | `%hi/%lo` | 不合并 | +| T10 | 安全冗余 LUI | 同块、同寄存器、同立即数 | 删除后一个 | +| T11 | 中间 clobber | `addi t0,t0,1` | 不删除 | +| T12 | 寄存器别名 | `t0` 与 `x5` | 正确识别修改 | +| T13 | 跨标签 | 标签两侧相同 `lui` | 不删除 | +| T14 | 跨分支 | 分支可能绕过前一 `lui` | 不删除 | +| T15 | 调用边界 | `call foo` | 清空状态 | +| T16 | 迭代暴露模式 | 删除冗余后出现相邻对 | 完成二次优化 | +| T17 | 幂等性 | 对结果再次优化 | 0 变化 | +| T18 | 空输入 | `""` | 空输出、0 变化 | +| T19 | 格式保持 | 首尾空行、缩进注释、纯空白行 | 零转换时逐字节不变,转换后再次运行逐字节不变 | +| T20 | 非整数寄存器 | `foo`、`x32`、`f0`、`v0` | 计为结构候选但安全拒绝,不转换 | + +### 7.2 集成测试 + +- **独立模块**:输入 `.s`,检查 `-o` 文件和 `-v`; +- **主 CLI**:检查 `--const-merge` 参数解析和配置映射; +- **CompilerDriver**:检查 post-pass 顺序; +- **汇编器**:优化前后均可汇编; +- **模拟器**:至少 3 个可执行用例结果一致; +- **回归**:全量测试通过。 + +### 7.3 建议 fixture + +```text +tests/fixtures/const_merge/ +├── basic_pair.s +├── sign_extension.s +├── redundant_lui.s +├── control_flow_guard.s +├── register_alias.s +└── relocation_noop.s +``` + +### 7.4 Benchmark 验证矩阵 + +| 类型 | 输入来源 | 目的 | 可以得出的结论 | +|---|---|---|---| +| 真实 case | `run_benchmark.py` 现有 ONNX/DSL case 生成的一份原始汇编 | 测主流水线实际模式覆盖与收益 | 当前后端是否产生目标模式,以及真实汇编/机器码是否变化 | +| Synthetic microbenchmark | `bench_const_merge.py` 固定 seed 人工汇编 | 测规则命中正确性、密度影响和 pass 开销 | 目标模式存在时的算法效果;不能外推为端到端收益 | +| 工具链等价用例 | 至少 3 个可执行 `.s` fixture | 验证汇编、反汇编和执行语义 | before/after 在限定输入上的语义一致性 | + +真实 case 报告必须逐 case 保留零命中结果,并提供汇总行;不得只展示发生变化的 case。`bench_runner.py` 可继续作为 DSL/IR 性能基线,但应在报告中注明它不覆盖 const-merge。 + +--- + +## 8. 验收标准(Definition of Done) + +- [ ] 设计文档已评审,范围和非目标明确; +- [x] `merge_constants` 现有公共接口保持兼容; +- [x] 使用共享汇编解析器; +- [x] `lui+addi` 正确处理 12 位符号扩展和 RV32 截断; +- [x] 冗余 `lui` 只在可证明安全的基本块内删除; +- [x] 正确处理 ABI/数字寄存器别名; +- [x] 不优化重定位表达式; +- [x] 实现固定点迭代和统计; +- [x] 新增正向、负向、边界和反例测试; +- [x] 未修改汇编文本逐字保留,转换结果满足字符串级幂等性; +- [x] 只接受合法 RV32 整数寄存器参与两类转换; +- [x] `pytest tests/test_const_merge.py -v` 通过; +- [x] 全量回归通过;TinyFive stub 的汇编加载与 32 位内存读写回归已修复; +- [ ] 至少 3 个汇编样例通过工具链或模拟器等价验证; +- [x] `run_benchmark.py` 基于同一份原始 RISC-V 汇编完成 const-merge A/B; +- [x] 真实 case 逐项报告候选数、分类转换数、有效汇编指令数和 pass 耗时; +- [x] 工具链不可用时明确标记机器指令、代码大小和执行等价为 `N/A`; +- [x] `bench_const_merge.py` 明确标记 synthetic,并覆盖 `lui+addi` 与重复 `lui` 两类密度; +- [x] Benchmark 不再使用换行数作为指令数,且零命中 case 不被过滤; +- [x] 文档明确说明 `li` 是伪指令; +- [x] 主 CLI `--const-merge` 可用; +- [ ] PR 描述包含前后示例、测试结果和已知限制。 + +--- + +## 9. 风险评估与依赖 + +| 风险项 | 影响程度 | 缓解措施 | +|---|---|---| +| 错误理解 `li` 的性能收益 | 高 | 用 objdump 展开验证;统计分类 | +| 符号扩展或位宽错误 | 高 | 数学公式 + 边界测试 | +| 跨块错误删除 | 高 | 在标签和控制流边界清空状态 | +| 寄存器别名错误 | 高 | 统一映射为 `xN` | +| 解析器重构导致格式回归 | 中 | 先写 characterization tests | +| 现有后端不产生 `lui+addi` | 中 | 用独立 `.s` fixture;记录命中率 | +| GNU 汇编器版本差异 | 低/中 | 限定测试命令和 `-march=rv32im` | +| 与调度器顺序冲突 | 中 | const-merge 固定在 scheduler 前 | + +- **Python 依赖**:项目现有依赖,原则上不新增第三方 Python 库; +- **可选工具链**:`binutils-riscv64-linux-gnu`、TinyFive、Spike; +- **兼容性**:默认不改变未开启 `--const-merge` 时的编译结果。 + +--- + +## 10. 开发进度跟踪 + +以下日期为建议示例,可按课程安排调整: + +| 阶段 | 计划完成日期 | 状态 | +|---|---|---| +| 仓库走读与基线记录 | 2026-07-29 | ✅ 已完成 | +| 设计文档评审 | 待维护者确认 | ⏳ 待评审 | +| Characterization tests | 2026-08-02 | ✅ 已完成 | +| 核心编码与重构 | 2026-08-15 | ✅ 已完成 | +| 边界测试与调试 | 2026-08-15 | ✅ 已完成 | +| 工具链等价验证 | 待工具链可用 | ⏸ N/A | +| 文档完善 | 2026-08-15 | ✅ 已完成 | +| 本地 AI review 与修订 | 2026-08-16 | ✅ 已完成 | + +--- + +## 11. 第一天实际工作清单(历史记录) + +以下内容保留为开发过程记录,不代表当前仍需新建分支或保持核心代码未修改: + +```bash +# 1. 进入仓库和环境 +cd ScratchV +source .venv/bin/activate + +# 2. 确认分支与基线 +git status +git rev-parse HEAD +pytest tests/test_const_merge.py -v + +# 3. 找到所有相关代码 +grep -R "const_merge\|merge_constants\|--const-merge" -n scratchv tests docs + +# 4. 阅读顺序 +# scratchv/backend/const_merge.py +# scratchv/backend/_asm_parser.py +# scratchv/compiler.py +# scratchv/main.py +# tests/test_const_merge.py +# scratchv/backend/instruction_select.py +# scratchv/backend/asm_emit.py + +# 5. 新建分支 +git switch -c feat/topic14-const-merge +``` + +阅读时建立一张表: + +| 问题 | 当前答案 | 证据文件/行 | 是否要修改 | +|---|---|---|---| +| 优化位于哪个阶段? | post-codegen | `compiler.py` | 否/确认 | +| 当前 CLI 名称? | `--const-merge` | `main.py` | 评审 | +| 当前是否迭代? | 待代码确认 | `const_merge.py` | 是 | +| 是否跨标签清空状态? | 待测试确认 | `const_merge.py` | 是 | +| 是否处理寄存器别名? | 待测试确认 | `const_merge.py` | 是 | +| 当前后端是否产生 `lui+addi`? | 常量通常直接输出 `li` | `instruction_select.py` | 记录限制 | + +当天结束时应得到: + +1. 基线测试结果; +2. 相关文件关系图; +3. 5–10 个明确测试用例; +4. 设计文档 v0.1; +5. 需要确认的问题列表; +6. 尚未修改核心代码的干净功能分支。 + +--- + +## 12. PR 描述建议结构 + +```markdown +## What +实现/改进 RV32 常量加载合并优化: +- 安全合并 lui+addi +- 基本块内冗余 lui 消除 +- 寄存器别名处理 +- 固定点迭代与统计 + +## Why +现有实现缺少若干控制流与边界测试,且重复维护汇编解析逻辑。 + +## Safety +- 不跨标签/分支/调用 +- 不处理重定位表达式 +- 按 RV32 语义截断 + +## Tests +- pytest tests/test_const_merge.py -v +- pytest tests/ -q +- GNU assembler + objdump 对比 + +## Limitations +- 不支持 RV64 完整常量构造 +- 不做跨基本块数据流分析 +- li 为伪指令,不保证减少最终机器指令 +``` + +--- + +## 13. 参考资料 + +- ScratchV 课程首页: +- 课题 14 课程页: +- 课题 14 归档说明: +- 当前实现: +- 当前共享解析器: +- 当前测试: +- 贡献指南: +- RISC-V RV32I 规范: diff --git "a/docs/\350\257\276\351\242\23014-\345\270\270\351\207\217\345\212\240\350\275\275\345\220\210\345\271\266\344\274\230\345\214\226-\346\212\200\346\234\257\350\256\276\350\256\241\346\226\207\346\241\243\345\210\235\347\250\277.md" "b/docs/\350\257\276\351\242\23014-\345\270\270\351\207\217\345\212\240\350\275\275\345\220\210\345\271\266\344\274\230\345\214\226-\346\212\200\346\234\257\350\256\276\350\256\241\346\226\207\346\241\243\345\210\235\347\250\277.md" new file mode 100644 index 0000000..aa5ceef --- /dev/null +++ "b/docs/\350\257\276\351\242\23014-\345\270\270\351\207\217\345\212\240\350\275\275\345\220\210\345\271\266\344\274\230\345\214\226-\346\212\200\346\234\257\350\256\276\350\256\241\346\226\207\346\241\243\345\210\235\347\250\277.md" @@ -0,0 +1,767 @@ +# ScratchV 课题 14:常量加载合并优化技术设计文档 + +> **文档版本**:v0.3(实现与本地验证状态回填) +> **编写日期**:2026-07-28 +> **更新日期**:2026-08-15 +> **作者**:[yuki] +> **关联 Issue**:[#待补充] +> **涉及模块**:`scratchv/backend/const_merge.py`、`scratchv/backend/_asm_parser.py`、`scratchv/compiler.py`、`scratchv/main.py` +> **目标架构**:RV32I/RV32IM,GNU Assembler(GAS)语法 +> **课题定位**:在现有参考实现基础上,完成安全性分析、重构设计、测试补全和编译器集成验证 + +--- + +## 0. 文档说明与工作假设 + +ScratchV 主分支已经存在 `scratchv/backend/const_merge.py` 参考实现,课程网站也将课题 14 标记为“已完成”。因此,本课题不应简单复制现有代码,而应完成以下工作: + +1. 理解并准确说明 `lui`、`addi` 与 `li` 的语义; +2. 分析当前实现的适用范围和潜在安全问题; +3. 设计一个行为明确、保守安全、可测试的汇编后处理优化; +4. 补充边界测试、集成测试和优化统计; +5. 明确区分“汇编文本行数减少”和“真实机器指令数减少”。 + +本初稿采用以下假设,提交评审前需要与指导教师或项目维护者确认: + +- 第一阶段只保证 **RV32** 语义,不处理 RV64 任意宽常量展开; +- 第一阶段只在 **单基本块内部** 消除冗余 `lui`,不做跨基本块数据流分析; +- 只处理数值立即数,不处理 `%hi(symbol)`、`%lo(symbol)` 等重定位表达式; +- 保持公共函数 `merge_constants(asm_text) -> tuple[str, int]` 兼容; +- ScratchV 主命令行开关使用当前代码中的 `--const-merge`,而不是归档题目中的 `--merge-constants`。 + +### 0.1 已实现结论 + +第一阶段已按 RV32、单基本块、纯数值立即数和兼容旧 API 的假设完成。固定点循环采用“先删除冗余 `lui`,再合并 `lui+addi`”,从而让删除重复加载后暴露出的序列可以继续合并。详细接口为 `merge_constants_detailed()`,寄存器别名由共享解析器的 `canonical_reg()` 统一处理,`is_integer_reg()` 在转换前拒绝非法名称以及浮点、向量等非整数寄存器。 + +--- + +## 一、功能介绍 + +### 1.1 背景 + +RISC-V 基础整数指令为定长编码。`addi` 只能携带 12 位有符号立即数,而 `lui` 将 20 位 U 型立即数放入目标寄存器的高 20 位、低 12 位补零。加载一般 32 位常量时,汇编器或编译器常使用: + +```asm +lui t0, 0x12345 +addi t0, t0, 0x678 +``` + +其结果为: + +```text +value = ((imm_hi & 0xFFFFF) << 12) + sign_extend_12(imm_lo) +``` + +在 RV32 中,最终结果按 32 位截断。 + +### 1.2 功能概述 + +常量加载合并优化是一个 **RISC-V 汇编层 post-pass**,在代码生成之后扫描汇编文本,执行两类转换: + +1. **常量序列规范化**:将符合条件的相邻 `lui + addi` 序列重写为 `li` 伪指令; +2. **冗余真实指令消除**:若同一物理寄存器在同一基本块中重复执行相同 `lui`,且中间没有被修改,则删除后一次 `lui`。 + +### 1.3 价值说明 + +- 统一常量加载的汇编表示,提高可读性; +- 删除可证明无用的 `lui`,减少真实机器指令; +- 为汇编级优化、指令统计和调度提供更规范的输入; +- 训练汇编解析、局部数据流跟踪、编译器 pass 集成和测试验证能力。 + +### 1.4 必须澄清的性能口径 + +`li` 是汇编器伪指令,不是 RV32I 的真实单条机器指令。对于较大的常量,汇编器通常仍会把 `li` 展开为 `lui + addi`。因此: + +- `lui + addi -> li` **必然减少汇编文本中的指令行数**; +- 但它 **不保证减少最终机器指令数或运行周期**; +- 真正可稳定减少机器指令的部分主要是安全的冗余 `lui` 消除; +- 若常量可用单条真实指令编码,例如有符号 12 位常量可使用 `addi rd, x0, imm`,则可以在后续增强版本中实现真实单指令替换。 + +文档、测试报告和优化统计不得把伪指令行数减少直接等同于机器指令数减少。 + +--- + +## 二、设计目标与非目标 + +### 2.1 设计目标 + +1. **语义正确**:正确处理 `addi` 12 位立即数的符号扩展和 RV32 截断; +2. **保守安全**:无法证明安全的序列不优化; +3. **基本块安全**:不跨标签、分支、跳转、调用和返回复用寄存器状态; +4. **别名正确**:把 `t0` 与 `x5` 等 ABI 名和数字寄存器名视为同一物理寄存器; +5. **幂等性**:对已优化结果再次运行不应继续发生变化; +6. **格式兼容**:尽可能保留标签、注释、空行和汇编指令顺序; +7. **可观测性**:统计合并对数、删除的冗余 `lui` 数和文本行数变化; +8. **可集成性**:支持独立模块 CLI 和 ScratchV 主编译流程开关。 + +### 2.2 非目标 + +第一阶段不实现: + +- RV64 任意 64 位常量的完整 `li` 展开与合并; +- `%hi(symbol)`、`%lo(symbol)`、`%pcrel_hi` 等重定位表达式优化; +- 跨基本块、跨循环或跨函数的全局常量传播; +- 基于控制流图的到达定义分析; +- 指令调度、寄存器分配或 IR 级常量折叠; +- 对所有 GNU/LLVM 汇编语法变体的完整支持。 + +--- + +## 三、当前系统分析 + +### 3.1 ScratchV 编译流水线中的位置 + +ScratchV 当前主要流程为: + +```text +DSL/ONNX 解析 + -> IR 优化 + -> RISC-V 指令选择 + -> 寄存器分配 + -> 汇编生成 + -> 汇编级 post-pass + 1. asm peephole + 2. const merge + 3. scheduler + 4. beautifier + 5. instruction counter + -> 输出 .s +``` + +常量加载合并属于 **汇编生成之后** 的局部优化,不修改 AST、IR 或机器指令数据结构。 + +### 3.2 当前代码接口 + +```python +from scratchv.backend.const_merge import merge_constants + +optimized_asm, changes = merge_constants(asm_text) +``` + +独立命令行: + +```bash +python -m scratchv.backend.const_merge input.s -o output.s -v +``` + +ScratchV 主命令行: + +```bash +scratchv input.dsl -o output.s --const-merge +``` + +### 3.3 历史基线问题(现已整改) + +以下问题记录的是 commit `109a6a2` 的开发前状态: + +1. `const_merge.py` 自己维护一套 `AsmInst` 解析逻辑,而项目已有共享的 `_asm_parser.py`; +2. 当前共享解析器的文档声称供 const-merge 使用,但实际参考实现仍重复解析代码; +3. 当前冗余 `lui` 跟踪没有明确在标签和控制流边界清空,跨基本块可能产生不安全删除; +4. 当前实现按字符串比较寄存器,`t0` 与 `x5` 的别名可能导致错误判断; +5. 现有测试主要覆盖正向样例,缺少标签、分支、寄存器别名、符号立即数、负十六进制和幂等性测试; +6. 归档课题要求“迭代扫描”,参考实现目前主要是一次合并加一次冗余消除; +7. 已在 commit `109a6a2` 核实:ScratchV 当前指令选择的多个常量路径直接生成 `MachineOp.LI`,所以主编译流程中 `lui + addi` 合并规则可能零命中;真实 case 必须测量并保留这一结果,同时用独立汇编 fixture 验证目标模式; +8. 当前 `changes` 只给总数,无法区分文本规范化与真实冗余指令删除。 + +--- + +## 四、语义模型与计算规则 + +### 4.1 12 位符号扩展 + +```python +def sign_extend_12(value: int) -> int: + """把 12 位编码(0..0xFFF)解释为有符号值。""" + value &= 0xFFF + return value - 0x1000 if value & 0x800 else value +``` + +边界: + +| 输入编码 | 符号扩展结果 | +|---|---:| +| `0x000` | 0 | +| `0x7FF` | 2047 | +| `0x800` | -2048 | +| `0xFFF` | -1 | + +### 4.2 RV32 最终常量 + +```python +upper_u32 = ((imm_hi & 0xFFFFF) << 12) & 0xFFFFFFFF +lower_s32 = sign_extend_12(imm_lo) +final_u32 = (upper_u32 + lower_s32) & 0xFFFFFFFF +final_s32 = final_u32 if final_u32 < 0x80000000 else final_u32 - 0x100000000 +``` + +输出 `li` 时建议统一使用有符号十进制 `final_s32`,避免 Python 无界整数与 RV32 位宽语义混淆。若项目更偏好十六进制输出,也必须明确按 32 位规范化。 + +### 4.3 示例 + +```asm +lui t0, 0x12345 +addi t0, t0, 0x678 +``` + +```text +0x12345000 + 0x678 = 0x12345678 +``` + +```asm +lui t0, 0x1 +addi t0, t0, 0x800 +``` + +`0x800` 作为 12 位立即数解释为 `-2048`: + +```text +0x00001000 - 0x800 = 0x00000800 +``` + +--- + +## 五、总体架构 + +### 5.1 模块职责 + +```text +输入汇编文本 + | + v +共享汇编解析器 parse_asm() + | + v +ParsedAsmLine 列表 + | + +--> 规则 A:lui + addi 合并 + | + +--> 规则 B:块内冗余 lui 消除 + | + +--> 固定点迭代与统计 + v +lines_to_asm() + | + v +输出汇编文本 + 统计信息 +``` + +### 5.2 建议数据结构 + +```python +from dataclasses import dataclass + +@dataclass +class ConstantMergeStats: + merged_pairs: int = 0 + redundant_lui_removed: int = 0 + iterations: int = 0 + + @property + def total_changes(self) -> int: + return self.merged_pairs + self.redundant_lui_removed +``` + +为保持兼容: + +```python +def merge_constants(asm_text: str) -> tuple[str, int]: + optimized, stats = ConstantMergeOptimizer().optimize(asm_text) + return optimized, stats.total_changes +``` + +详细统计可通过新增 API 获得,但第一阶段不强制修改已有调用方。 + +--- + +## 六、核心算法设计 + +### 6.1 汇编解析 + +优先复用: + +```python +from scratchv.backend._asm_parser import ( + ParsedAsmLine, + parse_asm, + lines_to_asm, + classify_def_use, +) +``` + +立即数解析建议使用: + +```python +def parse_numeric_imm(text: str) -> int | None: + try: + return int(text.strip(), 0) + except ValueError: + return None +``` + +这可同时处理十进制、十六进制及 `-0x1`。包含符号或重定位表达式时返回 `None`,放弃优化。 + +### 6.2 寄存器规范化 + +必须把 ABI 名称转换为统一的 `xN`: + +```text +t0 -> x5 +fp -> x8 +s0 -> x8 +a0 -> x10 +``` + +所有定义、使用和状态跟踪都使用规范化名称,避免别名漏判。 + +### 6.3 规则 A:`lui + addi -> li` + +#### 匹配条件 + +设第一条有效指令为: + +```asm +lui rd, imm_hi +``` + +第二条有效指令为: + +```asm +addi rd2, rs1, imm_lo +``` + +仅当以下条件全部成立时转换: + +1. 两条指令在同一基本块内; +2. 中间最多只有空行或纯注释,没有标签或其他有效指令; +3. `canonical(rd) == canonical(rd2) == canonical(rs1)`; +4. 两个立即数都是纯数值; +5. 第二条指令不能带可作为跳转目标的标签; +6. 操作数数量合法; +7. 计算结果可按 RV32 语义规范化。 + +#### 输出 + +```asm +li rd, final_s32 # merged lui+addi +``` + +若第一条指令带标签,则标签保留在新 `li` 上;原有注释按约定合并或保留。 + +#### 不转换示例 + +```asm +lui t0, 0x12345 +addi t1, t0, 0x678 # 目标寄存器不同 +``` + +```asm +lui t0, %hi(symbol) +addi t0, t0, %lo(symbol) # 重定位表达式 +``` + +```asm +lui t0, 1 +L1: +addi t0, t0, 2 # addi 是跳转目标 +``` + +### 6.4 规则 B:基本块内冗余 `lui` 消除 + +维护: + +```python +lui_state: dict[str, int] +``` + +表示当前基本块中,某物理寄存器最后一次可确认的 `lui` 高位立即数。 + +处理规则: + +1. 遇到 `lui rd, imm`: + - 若 `lui_state[canonical(rd)] == imm`,当前 `lui` 冗余,可删除,且保持原有状态不变; + - 否则更新状态。 +2. 遇到会定义某寄存器的指令:清除该寄存器状态; +3. 遇到标签、条件分支、无条件跳转、函数调用或返回:清空全部状态; +4. 遇到未知指令且无法可靠判断定义集合:一律清空全部状态; +5. 不能跨基本块使用线性扫描状态。 + +#### 安全示例 + +```asm +lui t0, 0x10000 +addi t1, t0, 16 +lui t0, 0x10000 # 可删除,t0 中间未被修改 +addi t2, t0, 32 +``` + +#### 不安全示例 + +```asm +beq a0, zero, L1 +lui t0, 0x10000 +L1: +lui t0, 0x10000 # 不能删除:分支可能跳过前一个 lui +``` + +### 6.5 固定点迭代 + +单次规则应用可能暴露新的匹配: + +```asm +lui t0, 1 +lui t0, 1 # 删除后 +addi t0, t0, 2 # 与前一个 lui 相邻,可继续合并 +``` + +因此采用: + +```text +repeat: + 应用规则 B + 应用规则 A +until 本轮无变化 or 达到最大迭代次数 +``` + +先应用规则 B,是为了在删除重复 `lui` 后暴露前一条 `lui` 与后续 `addi` 的匹配机会。 + +最大迭代次数可设为 `max(1, len(lines))` 或一个保守上限。每次转换都会减少有效指令数,因此算法必然终止。 + +### 6.6 复杂度 + +若每轮线性扫描为 `O(n)`,最坏迭代次数为 `O(n)`,上界 `O(n^2)`。在典型汇编文件中迭代次数通常为 1–2。若需要严格线性复杂度,可调整规则顺序或使用工作队列,但不属于第一阶段目标。 + +--- + +## 七、接口设计 + +### 7.1 Python API + +兼容接口: + +```python +def merge_constants(asm_text: str) -> tuple[str, int]: + """返回优化后的汇编文本和转换总数。""" +``` + +建议新增详细接口: + +```python +def merge_constants_detailed( + asm_text: str, + *, + max_iterations: int | None = None, +) -> tuple[str, ConstantMergeStats]: + """返回优化后的汇编文本和分类统计。""" +``` + +### 7.2 独立 CLI + +```bash +python -m scratchv.backend.const_merge input.s -o output.s -v +``` + +`-v` 输出: + +```text +Constant merge: + merged lui+addi pairs: 3 + redundant lui removed: 2 + total transformations: 5 + iterations: 2 +``` + +### 7.3 ScratchV 主编译器开关 + +当前项目接口: + +```bash +scratchv model.onnx -o output.s --const-merge +scratchv --dsl example.dsl -o output.s --const-merge +``` + +配置字段: + +```python +CompilerConfig(const_merge=True) +``` + +### 7.4 Pass 顺序 + +建议维持: + +```text +asm peephole -> const merge -> scheduler -> beautifier -> instruction counter +``` + +原因: + +- 窥孔优化可能暴露新的相邻常量序列; +- 调度器可能打乱相邻关系,应在常量合并之后运行; +- 指令统计应在所有优化完成后运行。 + +--- + +## 八、正确性与安全约束 + +优化必须满足: + +```text +对任意允许的初始寄存器状态和执行路径, +优化前后程序在可观察行为上等价。 +``` + +第一阶段采用保守策略: + +- 不跨标签; +- 不跨控制流指令; +- 不跨调用; +- 不处理符号立即数; +- 不对未知操作码做激进定义/使用推断; +- 规范化寄存器别名; +- 仅允许 `x0` 至 `x31` 及其 psABI 别名参与整数常量转换; +- 数字标签、包含 `$` 的标签及无法分类的非空汇编行均作为状态边界; +- 仅重建被转换的指令,其他源文本逐字保留,避免改变字符串伪操作等数据; +- 按 RV32 位宽截断。 + +立即数安全范围也在变换前显式验证:`lui` 接受有符号 20 位写法或 `0..0xFFFFF` 的编码字段写法,`addi` 接受有符号 12 位写法或 `0..0xFFF` 的编码字段写法。超出范围的数值和 `%hi/%lo` 等符号表达式均保持原样,不做静默截断。 + +--- + +## 九、测试设计 + +### 9.1 单元测试分类 + +#### A. 汇编解析 + +- 十进制、十六进制、负十进制、负十六进制; +- 普通标签、数字标签、包含 `$` 的标签、注释和空行; +- `0(sp)` 等带括号操作数; +- 指令重建后内容可用; +- 无论是否发生转换,未修改的字符串伪操作、标签、首尾空行、纯空白行和注释缩进逐字节保留。 + +#### B. `lui + addi` 合并 + +- 正常大常量 `0x12345678`; +- 低 12 位边界 `0x7FF`; +- 符号扩展边界 `0x800`; +- `0xFFF` 对应 `-1`; +- 结果为 `0x80000000`; +- 结果溢出后按 RV32 截断; +- ABI/数字寄存器别名混用; +- 中间有注释或空行; +- 中间有标签时不合并; +- 目标寄存器或源寄存器不同时不合并; +- `%hi/%lo` 不合并。 +- 非法名称、`x32`、浮点和向量寄存器不合并。 + +#### C. 冗余 `lui` 消除 + +- 同寄存器同立即数且未修改:删除; +- 同寄存器不同立即数:不删除; +- 中间写目标寄存器:不删除; +- 中间通过别名写目标寄存器:不删除; +- 跨标签:不删除; +- 跨分支、跳转、调用、返回:不删除; +- 未知操作码:保守处理。 + +#### D. 属性测试 + +- 字符串级幂等性:`opt(opt(x)) == opt(x)`,包括转换后位于文件末尾的注释和空行; +- 无匹配输入:输出语义和有效指令不变; +- 转换总数与分类统计一致; +- 固定点迭代可发现删除后暴露的新模式。 + +### 9.2 集成测试 + +1. 独立 CLI 读取输入文件并生成输出文件; +2. `--verbose` 输出正确统计; +3. ScratchV 主 CLI 的 `--const-merge` 能正确传递到 `CompilerConfig`; +4. 编译器 post-pass 顺序符合设计; +5. 使用 GNU assembler 对优化前后汇编分别汇编; +6. 使用 `objdump -d -M no-aliases` 比较 `li` 的真实展开; +7. 使用 TinyFive、Spike 或项目模拟器执行可运行样例,确认最终寄存器/返回值相同。 + +### 9.3 关键验收用例 + +#### 用例 1:符号扩展 + +```asm +lui t0, 0x1 +addi t0, t0, 0x800 +``` + +期望: + +```asm +li t0, 2048 +``` + +#### 用例 2:安全冗余消除 + +```asm +lui t0, 0x10000 +addi t1, t0, 16 +lui t0, 0x10000 +addi t2, t0, 32 +``` + +期望:删除第二条 `lui`,其他指令不变。 + +#### 用例 3:跨块不得删除 + +```asm +beq a0, zero, L1 +lui t0, 0x10000 +L1: +lui t0, 0x10000 +``` + +期望:两个 `lui` 均保留。 + +#### 用例 4:寄存器别名 + +```asm +lui t0, 0x10000 +addi x5, x5, 1 +lui t0, 0x10000 +``` + +期望:第二条 `lui` 保留,因为 `x5` 与 `t0` 是同一寄存器。 + +--- + +## 十、Benchmark 设计 + +### 10.1 历史覆盖缺口与当前状态 + +基于 commit `109a6a2` 的历史缺口及当前整改结果如下: + +| 路径 | 当前行为 | 对课题 14 的覆盖 | +|---|---|---| +| `benchmarks/bench_runner.py` | 运行 DSL/IR 解释与性能 case | 不生成 RISC-V 汇编,不经过 const-merge | +| `benchmarks/run_benchmark.py` | ONNX 解析、IR 优化、RISC-V/LLVM codegen | 已对同一原始汇编执行 const-merge A/B,并保留零命中和 `N/A` | +| `benchmarks/bench_const_merge.py` | 固定 seed 生成人工模式并计时 | 已覆盖两条规则、两类密度、分类统计和有效指令计数 | + +因此,本课题不能只修改 `bench_const_merge.py` 后声称优化已体现在“项目 benchmark case”中。设计必须同时提供真实 case A/B 和 synthetic microbenchmark,两者结论分开呈现。 + +### 10.2 真实 case 的单变量 A/B + +真实 case 复用 `run_benchmark.py` 的现有 ONNX/DSL 输入。在 RISC-V codegen 得到 `asm_before` 后,只对同一字符串调用一次 const-merge: + +```python +asm_before = asm_str +t0 = time.perf_counter() +asm_after, stats = merge_constants_detailed(asm_before) +pass_time_ms = (time.perf_counter() - t0) * 1000 +``` + +禁止为 A/B 重新运行两次完整编译,避免前端、IR 优化、指令选择或寄存器分配成为额外变量。禁止修改真实 case 以人工插入 `lui+addi`。 + +逐 case 记录: + +```text +case_name +lui_count_before +candidate_pairs +merged_pairs +redundant_lui_removed +asm_instructions_before / asm_instructions_after +machine_instructions_before / machine_instructions_after +code_size_before / code_size_after +pass_time_ms +output_equal +``` + +其中 `candidate_pairs` 是在同一基本块内满足 `lui`/`addi` 操作码、操作数数量及相邻条件(中间只允许注释或空行)、可进入规则 A 安全检查的结构候选数。寄存器不一致、寄存器名称非法、立即数不可解析或越界的结构候选仍计入该字段,但不计入 `merged_pairs`。因此两者之差表示被安全检查拒绝的候选;标签、指令或跨块序列不计入候选。 + +### 10.3 有效汇编指令计数 + +`asm_text.count("\n")` 和 `len(asm_text.splitlines())` 都不是指令计数。计数器应解析每行并排除: + +- 空行与纯注释; +- 只有标签的行; +- `.text`、`.globl`、`.section` 等汇编伪操作。 + +标签与指令位于同一行时只计该指令。该口径用于 `asm_instructions_before/after`;它仍包含 `li` 等伪指令,因此不能替代机器指令数。 + +### 10.4 机器码与语义指标 + +工具链可用时,before/after 分别经 assembler 与 `objdump -d -M no-aliases`,统计真实机器指令和 `.text` 大小。`lui+addi -> li` 可能重新展开为两条机器指令,所以允许出现: + +```text +asm_instructions_after < asm_instructions_before +machine_instructions_after == machine_instructions_before +``` + +`output_equal` 只能由相同输入下的模拟器/执行结果比较得出;只完成 IR verifier、只成功汇编或只比较文本时记为 `N/A`,不能视为语义等价证明。外部工具不可用时,机器码、代码大小与执行等价字段均保留并标记 `N/A`,不得静默省略。 + +### 10.5 Synthetic microbenchmark + +`bench_const_merge.py` 继续负责可控压力测试,并明确打印 `benchmark_type=synthetic`。输入参数至少包括: + +- `num_instructions`:有效汇编指令规模; +- `pair_density`:`lui+addi` 模式密度; +- `redundant_lui_density`:同块重复 `lui` 模式密度; +- `seed` 与 `repeats`:可重复性及计时稳定性。 + +输出分类统计、有效指令差值及 pass 时间分布。人工构造的收益只能证明目标模式存在时优化器工作正常,并用于观察复杂度;不能外推为 ONNX/DSL 端到端收益。 + +### 10.6 结果解释与验收 + +真实 case 可能全部得到零候选,这是当前后端直接生成 `MachineOp.LI` 时的合理结果。报告必须保留所有 case(包括零值),并使用以下结论: + +> 当前真实代码生成路径没有产生 const-merge 的目标模式,因此本组 case 命中率为 0;synthetic microbenchmark 仅用于验证目标模式存在时的正确性和 pass 开销。 + +Benchmark 验收条件: + +1. 同一份原始汇编完成 A/B,唯一变量是 const-merge; +2. 逐 case 和汇总结果均包含候选、两类转换、有效指令数及耗时; +3. 工具链指标与 `output_equal` 不可测时明确为 `N/A`; +4. synthetic 输入同时覆盖两条优化规则,并固定 seed; +5. 满足 `total_changes == merged_pairs + redundant_lui_removed`; +6. 不将源汇编减少直接表述为机器指令、代码大小或周期减少。 + +--- + +## 十一、风险与权衡 + +| 风险 | 影响 | 缓解措施 | +|---|---|---| +| 把 `li` 当作真实单指令 | 高 | 区分文本行数、伪指令数和机器指令数 | +| `addi` 符号扩展错误 | 高 | 边界测试覆盖 `0x7FF/0x800/0xFFF` | +| 跨基本块错误删除 | 高 | 标签和控制流边界清空状态 | +| 寄存器 ABI 别名漏判 | 高 | 所有寄存器先规范化为 `xN` | +| 重定位表达式被错误解析 | 高 | 只接受纯数值立即数 | +| 注释或标签丢失 | 中 | 复用共享解析器并增加格式测试 | +| 当前后端已直接输出 `li`,命中率低 | 中 | 增加独立汇编 fixture;报告实际命中率 | +| 与调度器 pass 顺序冲突 | 中 | 固定在调度器之前运行 | +| 重构公共解析器引入回归 | 中 | 先补 characterization tests,再替换解析实现 | + +--- + +## 十二、待评审问题 + +提交设计评审时需要明确回答: + +1. 本课题是重现课程参考实现,还是改进主分支现有实现? +2. 第一阶段目标是 RV32 还是同时支持 RV64? +3. `lui+addi -> li` 的主要目标是代码可读性,还是需要证明真实机器指令减少? +4. 是否要求跨基本块优化?若要求,需要引入 CFG 和数据流分析,不应使用简单线性状态; +5. 是否必须沿用 `merge_constants(...)->(str, int)`,还是允许新增统计对象? +6. 是否需要把现有 `const_merge.py` 迁移到共享 `_asm_parser.py`? +7. 最终命令行名称以当前代码的 `--const-merge` 为准,还是兼容归档文档的 `--merge-constants`? +8. 真实 case A/B 是否合入 `run_benchmark.py` 的统一 JSON schema? +9. CI 是否提供 GNU RISC-V 工具链或模拟器;若不提供,机器码和语义指标是否作为可选阶段? +10. 零命中是否接受为当前主流水线的正式实验结论? + +--- + +## 十三、参考资料 + +- ScratchV 课程首页: +- 课题 14 当前课程页: +- 课题 14 归档说明: +- 当前参考实现: +- 当前测试: +- RISC-V ISA Manual: +- ScratchV 贡献指南: diff --git a/scratchv/backend/_asm_parser.py b/scratchv/backend/_asm_parser.py index 3cdef7e..d92f3d7 100644 --- a/scratchv/backend/_asm_parser.py +++ b/scratchv/backend/_asm_parser.py @@ -57,7 +57,7 @@ def to_asm(self) -> str: """Reconstruct the assembly line.""" # Empty lines if self.opcode is None and self.label is None and not self.raw.strip(): - return "" + return self.raw # Comment-only lines if self.opcode is None and self.label is None and self.comment: return self.raw @@ -108,7 +108,7 @@ def is_comment_only(self) -> bool: _LINE_RE = re.compile( r'^\s*' - r'(?P