课题6:编译器性能测试套件
难度:中 | 类型:项目实战 | 源文件:
benchmarks/bench_runner.py| 行数:~400 状态:✅ 已完成
概述
设计一组基准测试程序(DSL用例),自动化执行编译、模拟运行、对比预期输出,生成性能报告。该套件用于持续验证编译器的正确性和性能变化。
理解背景
是什么?
性能基准套件(Benchmark Suite)自动化测试 ScratchV 编译器的正确性和性能。它收集了 23 个 DSL 测试用例,覆盖算术、神经网络算子、控制流等各类场景,每次运行自动生成 JSON/HTML/Markdown 报告。
为什么?
编译器每次改动都可能引入 bug 或性能衰退。手工测试既不全面也不可靠。自动化的基准测试套件让你: - 回归检测:改了代码后跑一遍,立即知道有没有破坏现有功能 - 性能追踪:记录每次运行的时间,发现性能衰退 - CI 集成:每次 push 自动运行,阻止有问题的代码合入
核心概念
测试用例格式
每个测试用例最多 2 个文件(放在 benchmarks/cases/):
001_simple_add.dsl ← DSL 源代码(必需)
001_simple_add.expected ← 期望输出(可选)
DSL 语法要点
数字可以直接内联,变量名自动解析:
# 正确写法
c = add(a, b) # a, b 作为输入变量
t1 = add(2.0, 3.0) # 数字直接写
return c
# 错误写法(没有 const 操作符!)
a = const(3) # ❌ 不支持
支持的算子:add, sub, mul, div, neg, exp, relu, gelu, dot, matmul, softmax, maxpool
23 个用例覆盖范围
| 类别 | 数量 | 示例 |
|---|---|---|
| 算术 | 5 | add, mul, sub, div, chained |
| 神经网络 | 6 | relu, gelu, softmax, matmul, dot, maxpool |
| 控制流 | 7 | for-loop, if/else, while, nested |
| 复杂场景 | 3 | NN pipeline, large chain |
| 常量 | 1 | constant propagation |
报告格式
| 格式 | 用途 |
|---|---|
| 终端输出 | 开发时快速查看 |
| JSON | CI dashboard 数据源 |
| HTML | 可视化展示 |
| Markdown | 文档、PR review |
代码走读
运行全部测试
# 基础运行(指定用例目录)
python3 benchmarks/bench_runner.py benchmarks/cases
# 生成 JSON + HTML 报告
python3 benchmarks/bench_runner.py benchmarks/cases \
--output-json /tmp/dsl_bench.json \
--output-html /tmp/dsl_bench.html
# 基础运行(指定用例目录)
python3 benchmarks/bench_runner.py benchmarks/cases
# 生成 JSON + HTML 报告
python3 benchmarks/bench_runner.py benchmarks/cases \
--output-json /tmp/dsl_bench.json \
--output-html /tmp/dsl_bench.html
实际输出(23/23 PASS):
Discovered 23 test case(s) in benchmarks/cases
------------------------------------------------------------
[1/23] 001_simple_add ... PASS (0.008s parse, 2 inst)
[2/23] 002_simple_mul ... PASS (0.000s parse, 2 inst)
...
[23/23] 023_large_chain ... PASS (0.000s parse, 7 inst)
用 Makefile 一键运行
make bench # 等价于上面的完整流程
添加新测试用例
cat > benchmarks/cases/024_my_test.dsl << 'EOF'
# 我的自定义测试: 10 + 20 - 5
a = add(10, 20)
b = sub(a, 5)
return b
EOF
echo "25" > benchmarks/cases/024_my_test.expected
# 运行确认
python3 benchmarks/bench_runner.py benchmarks/cases
💡 关键:数字直接内联使用(如 10, 20),不需要 const() 包装。期望输出文件是纯文本,末尾不要有多余空行。
Python API 用法
from benchmarks.bench_runner import BenchmarkRunner
# 创建 runner
runner = BenchmarkRunner(
test_dir="benchmarks/cases",
timeout=30.0,
verbose=True,
)
# 发现测试用例
cases = runner.discover_cases()
print(f"Found {len(cases)} test cases")
# 运行全部(返回 BenchmarkReport)
report = runner.run_all()
report.print_summary()
# 生成报告
report.save_json("/tmp/results.json")
report.save_html("/tmp/results.html")
report.save_markdown("/tmp/results.md")
CI 回归检测
# 生成基线
python3 benchmarks/bench_runner.py benchmarks/cases --output-json /tmp/baseline.json
# 修改代码后...
python3 benchmarks/bench_runner.py benchmarks/cases --output-json /tmp/current.json
# 对比通过率
python3 -c "
import json
baseline = json.load(open('/tmp/baseline.json'))
current = json.load(open('/tmp/current.json'))
if current['pass_rate'] < baseline['pass_rate']:
print('REGRESSION DETECTED!')
exit(1)
else:
print('OK: pass_rate stable')
"
动手练习
练习 1: 添加一个新测试用例
make bench # 等价于上面的完整流程
cat > benchmarks/cases/024_my_test.dsl << 'EOF'
# 我的自定义测试: 10 + 20 - 5
a = add(10, 20)
b = sub(a, 5)
return b
EOF
echo "25" > benchmarks/cases/024_my_test.expected
# 运行确认
python3 benchmarks/bench_runner.py benchmarks/cases
💡 关键:数字直接内联使用(如
10,20),不需要const()包装。期望输出文件是纯文本,末尾不要有多余空行。
Python API 用法
from benchmarks.bench_runner import BenchmarkRunner
# 创建 runner
runner = BenchmarkRunner(
test_dir="benchmarks/cases",
timeout=30.0,
verbose=True,
)
# 发现测试用例
cases = runner.discover_cases()
print(f"Found {len(cases)} test cases")
# 运行全部(返回 BenchmarkReport)
report = runner.run_all()
report.print_summary()
# 生成报告
report.save_json("/tmp/results.json")
report.save_html("/tmp/results.html")
report.save_markdown("/tmp/results.md")
CI 回归检测
# 生成基线
python3 benchmarks/bench_runner.py benchmarks/cases --output-json /tmp/baseline.json
# 修改代码后...
python3 benchmarks/bench_runner.py benchmarks/cases --output-json /tmp/current.json
# 对比通过率
python3 -c "
import json
baseline = json.load(open('/tmp/baseline.json'))
current = json.load(open('/tmp/current.json'))
if current['pass_rate'] < baseline['pass_rate']:
print('REGRESSION DETECTED!')
exit(1)
else:
print('OK: pass_rate stable')
"
动手练习
练习 1: 添加一个新测试用例
from benchmarks.bench_runner import BenchmarkRunner
# 创建 runner
runner = BenchmarkRunner(
test_dir="benchmarks/cases",
timeout=30.0,
verbose=True,
)
# 发现测试用例
cases = runner.discover_cases()
print(f"Found {len(cases)} test cases")
# 运行全部(返回 BenchmarkReport)
report = runner.run_all()
report.print_summary()
# 生成报告
report.save_json("/tmp/results.json")
report.save_html("/tmp/results.html")
report.save_markdown("/tmp/results.md")
# 生成基线
python3 benchmarks/bench_runner.py benchmarks/cases --output-json /tmp/baseline.json
# 修改代码后...
python3 benchmarks/bench_runner.py benchmarks/cases --output-json /tmp/current.json
# 对比通过率
python3 -c "
import json
baseline = json.load(open('/tmp/baseline.json'))
current = json.load(open('/tmp/current.json'))
if current['pass_rate'] < baseline['pass_rate']:
print('REGRESSION DETECTED!')
exit(1)
else:
print('OK: pass_rate stable')
"
动手练习
练习 1: 添加一个新测试用例
为你正在开发的编译器功能写一个测试用例,跑通并确认通过。
练习 2: 故意引入 bug
修改代码引入一个 bug(比如把 ADD 映射成 SUB),跑 benchmark 看看哪些测试会失败。
练习 3: 分析性能趋势
连续跑 3 次 benchmark,对比 JSON 输出,看编译时间是否有波动。
常见坑
| 坑 | 说明 |
|---|---|
没有 const 操作符 |
DSL 不支持 a = const(3),直接用数字 a = add(3, 5) |
| expected 文件格式 | .expected 文件应该是纯文本,末尾不要有多余换行 |
| 超时 | 某些测试可能死循环,BenchmarkRunner 有 timeout 保护 |