Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
231 changes: 168 additions & 63 deletions benchmarks/bench_asm_beautifier.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# flake8: noqa
"""Benchmark for RISC-V Assembly Beautifier.
"""Stress benchmark for RISC-V Assembly Beautifier.

Measures beautification time and output size for assembly files
of varying complexity.
Expand All @@ -12,9 +12,23 @@
from __future__ import annotations

import argparse
import time
import random
import statistics
from typing import Optional
import sys
import time

from pathlib import Path
from typing import Any


PROJECT_ROOT = Path(__file__).resolve().parents[1]

# 仅当以脚本方式直接运行时,才把项目根加入导入路径,避免该模块被当作
# 普通模块导入时污染全局 sys.path 并改变其它导入顺序。推荐改用
# `python -m benchmarks.bench_asm_beautifier` 运行,此时无需修改 sys.path。
if __name__ == "__main__":
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))

from scratchv.backend.asm_beautifier import beautify_asm

Expand All @@ -23,8 +37,7 @@
# Test programs of varying complexity
# ---------------------------------------------------------------------------

_SIMPLE_ASM = """
.text
_SIMPLE_ASM = """.text
main:
addi sp, sp, -16
sw ra, 12(sp)
Expand All @@ -34,8 +47,7 @@
ret
"""

_MODERATE_ASM = """
.text
_MODERATE_ASM = """.text
main:
addi sp, sp, -32
sw ra, 28(sp)
Expand All @@ -60,63 +72,135 @@
ret
"""

_LARGE_ASM = _MODERATE_ASM * 20 # Duplicate for size
_LARGE_ASM = _MODERATE_ASM * 20


def _gen_random_asm(num_instrs: int, seed: int = 42) -> str:
"""Generate synthetic RISC-V assembly of a given size."""
import random
random.seed(seed)

ops = ["add", "sub", "addi", "lw", "sw", "beq", "j", "li", "mv", "mul",
"xor", "or", "and", "slli", "srli", "slt", "div", "jal", "ret"]
regs = ["t0", "t1", "t2", "t3", "t4", "t5", "t6",
"a0", "a1", "a2", "a3", "s0", "s1", "s2"]

lines = [".text", "synthetic_func:"]
for i in range(num_instrs):
op = random.choice(ops)
if op in ("j", "jal"):
lines.append(f" {op} label_{i % 10}")
elif op in ("beq", "bne", "blt", "bge"):
lines.append(f" {op} {random.choice(regs)}, {random.choice(regs)}, label_{i % 10}")
elif op == "li":
lines.append(f" {op} {random.choice(regs)}, {random.randint(0, 4096)}")
elif op == "mv":
lines.append(f" {op} {random.choice(regs)}, {random.choice(regs)}")
elif op in ("lw", "sw"):
lines.append(f" {op} {random.choice(regs)}, {random.randint(0, 16)}(sp)")
elif op in ("addi", "slli", "srli"):
lines.append(f" {op} {random.choice(regs)}, {random.choice(regs)}, {random.randint(0, 31)}")
"""Generate deterministic synthetic RISC-V assembly of a given size."""

generator = random.Random(seed)
arithmetic_ops = [
"add",
"sub",
"mul",
"xor",
"or",
"and",
"sll",
"slt",
"div",
]
immediate_ops = ["addi", "slli", "srli"]
registers = [
"t0",
"t1",
"t2",
"t3",
"t4",
"t5",
"t6",
"a0",
"a1",
"a2",
"a3",
"s0",
"s1",
"s2",
]
ops = [
*arithmetic_ops,
*immediate_ops,
"lw",
"sw",
"beq",
"j",
"li",
"mv",
"ret",
]

lines = [".text", ".globl synthetic_func", "synthetic_func:"]
for index in range(num_instrs):
opcode = generator.choice(ops)
label = f"label_{index % 10}"
if opcode == "j":
lines.append(f" {opcode} {label}")
elif opcode == "beq":
lines.append(
f" {opcode} {generator.choice(registers)}, "
f"{generator.choice(registers)}, {label}"
)
elif opcode == "li":
lines.append(
f" {opcode} {generator.choice(registers)}, "
f"{generator.randint(0, 4096)}"
)
elif opcode == "mv":
lines.append(
f" {opcode} {generator.choice(registers)}, "
f"{generator.choice(registers)}"
)
elif opcode in {"lw", "sw"}:
lines.append(
f" {opcode} {generator.choice(registers)}, "
f"{generator.randint(0, 16)}(sp)"
)
elif opcode in immediate_ops:
lines.append(
f" {opcode} {generator.choice(registers)}, "
f"{generator.choice(registers)}, {generator.randint(0, 31)}"
)
elif opcode == "ret":
lines.append(" ret")
else:
lines.append(f" {op} {random.choice(regs)}, {random.choice(regs)}, {random.choice(regs)}")
# Occasionally add labels
if i % 15 == 0:
lines.append(f"label_{i % 10}:")
lines.append(" ret\n")
lines.append(
f" {opcode} {generator.choice(registers)}, "
f"{generator.choice(registers)}, {generator.choice(registers)}"
)

if index % 15 == 0:
lines.append(f"label_{index % 10}:")
lines.append(" ret")
return "\n".join(lines)


# ---------------------------------------------------------------------------
# Benchmarks
# ---------------------------------------------------------------------------

def bench_beautify(asm_text: str, align: bool = True,
add_comments: bool = True,
repeats: int = 50) -> dict:
"""Run beautify benchmark and return timing statistics."""
times = []
def bench_beautify(
asm_text: str,
align: bool = True,
add_comments: bool = True,
abi_register_names: bool = False,
repeats: int = 50,
) -> dict[str, Any]:
"""Run beautification repeatedly and return timing statistics."""

if repeats < 1:
raise ValueError("repeats must be at least 1")

times: list[float] = []
output_size = 0
expected_result: str | None = None

for _ in range(repeats):
t0 = time.perf_counter()
result = beautify_asm(asm_text, align=align, add_comments=add_comments)
t1 = time.perf_counter()
times.append(t1 - t0)
start = time.perf_counter()
result = beautify_asm(
asm_text,
align=align,
add_comments=add_comments,
abi_register_names=abi_register_names,
)
times.append(time.perf_counter() - start)
output_size = len(result)
if expected_result is None:
expected_result = result
elif result != expected_result:
raise RuntimeError("beautifier output changed between runs")

return {
"input_lines": asm_text.count("\n"),
"input_lines": len(asm_text.splitlines()),
"input_chars": len(asm_text),
"output_chars": output_size,
"ratio": output_size / max(len(asm_text), 1),
Expand All @@ -125,12 +209,13 @@ def bench_beautify(asm_text: str, align: bool = True,
"max_s": max(times),
"mean_s": statistics.mean(times),
"median_s": statistics.median(times),
"stdev_s": statistics.stdev(times) if len(times) > 1 else 0,
"stdev_s": statistics.stdev(times) if len(times) > 1 else 0.0,
}


def run_all_benchmarks(repeats: int = 50):
"""Run all beautifier benchmarks."""
def run_all_benchmarks(repeats: int = 50) -> None:
"""Run fixed-size and synthetic beautifier benchmarks."""

benchmarks = {
"simple": _SIMPLE_ASM,
"moderate": _MODERATE_ASM,
Expand All @@ -142,36 +227,56 @@ def run_all_benchmarks(repeats: int = 50):
print("=" * 80)
print("RISC-V Assembly Beautifier Benchmark")
print("=" * 80)
print(f"{'Test':<20} {'Input':>8} {'Output':>8} {'Ratio':>7} {'Mean(ms)':>10} {'Stdev(ms)':>10}")
print(
f"{'Test':<20} {'Input':>8} {'Output':>8} {'Ratio':>7} "
f"{'Mean(ms)':>10} {'Stdev(ms)':>10}"
)
print("-" * 80)

for name, asm in benchmarks.items():
stats = bench_beautify(asm, repeats=repeats)
for name, asm_text in benchmarks.items():
stats = bench_beautify(asm_text, repeats=repeats)
print(
f"{name:<20} {stats['input_chars']:>8} "
f"{stats['output_chars']:>8} {stats['ratio']:>6.2f}x "
f"{stats['mean_s'] * 1000:>10.3f} {stats['stdev_s'] * 1000:>10.3f}"
f"{stats['mean_s'] * 1000:>10.3f} "
f"{stats['stdev_s'] * 1000:>10.3f}"
)

# Compare with and without features
print()
print("Feature Impact (on synthetic_1k):")
print("-" * 60)

asm = _gen_random_asm(1000)
synthetic_1k = benchmarks["synthetic_1k"]
for align in (True, False):
for comments in (True, False):
stats = bench_beautify(asm, align=align, add_comments=comments,
repeats=repeats)
stats = bench_beautify(
synthetic_1k,
align=align,
add_comments=comments,
repeats=repeats,
)
label = f"align={align}, comments={comments}"
print(f" {label:<30} {stats['mean_s'] * 1000:>8.3f} ms "
f"output: {stats['output_chars']} chars")
print(
f" {label:<30} {stats['mean_s'] * 1000:>8.3f} ms "
f"output: {stats['output_chars']} chars"
)


def _positive_int(value: str) -> int:
repeats = int(value)
if repeats < 1:
raise argparse.ArgumentTypeError("repeats must be at least 1")
return repeats


def main():
def main() -> None:
parser = argparse.ArgumentParser(description="Beautifier Benchmark")
parser.add_argument("--repeats", type=int, default=50,
help="Number of repeat measurements")
parser.add_argument(
"--repeats",
type=_positive_int,
default=50,
help="number of repeat measurements",
)
args = parser.parse_args()
run_all_benchmarks(args.repeats)

Expand Down
Loading
Loading