Skip to content

Commit a94d261

Browse files
k1nsomclaude
andcommitted
docs: 中级3篇 + 高级5篇 — 文档改造核心完成
新增中级 (3篇): - Topic 06: 性能基准套件 (23个DSL用例 + 自动回归检测) - Topic 14: 常量加载合并 (lui+addi合并 + 冗余lui消除 + 符号扩展) - Topic 20: 代码规范 (Black/isort/Ruff/mypy + pre-commit钩子) 新增高级 (5篇): - Topic 21: IR验证器 (7条规则: SSA/def-before-use/label/block-term等) - Topic 22: Standalone LLVM编译器 (float32路径 + GEP + --compare) - Topic 23: Cache模型 (组相联+LRU + I$/D$ + compulsory/conflict miss) - Topic 28: 扩展指令选择 (sqrt/abs/branchless min + float64 D扩展) - Topic 30: CI Dashboard (编排器 + 纯静态HTML + 两层粒度 + badge评分) 清理: - backend_const_merge/inst_select_ext/standalone_llvm_compiler 归档到 archive - topics/INDEX.md 全量同步 (中级11/14 ✅ + 高级5/9 ✅) 文档总进度: 5篇总文档 + 23篇topic (入门7+中级11+高级5) = 28篇 ✅ 剩余: 中级3篇(10,15,27预留) + 高级4篇(24,25,26,29预留) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 48f9160 commit a94d261

12 files changed

Lines changed: 1279 additions & 12 deletions
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
# Topic 06 — 编译器性能测试套件
2+
3+
> **难度**: 中级 | **源文件**: `benchmarks/bench_runner.py`, `benchmarks/cases/`
4+
5+
---
6+
7+
## 是什么?
8+
9+
性能基准套件(Benchmark Suite)自动化测试 ScratchV 编译器的**正确性**和**性能**。它收集了 23 个 DSL 测试用例,覆盖算术、神经网络算子、控制流等各类场景,每次运行自动生成 JSON/HTML/Markdown 报告。
10+
11+
---
12+
13+
## 为什么?
14+
15+
编译器每次改动都可能引入 bug 或性能衰退。手工测试既不全面也不可靠。自动化的基准测试套件让你:
16+
- **回归检测**:改了代码后跑一遍,立即知道有没有破坏现有功能
17+
- **性能追踪**:记录每次运行的时间,发现性能衰退
18+
- **CI 集成**:每次 push 自动运行,阻止有问题的代码合入
19+
20+
---
21+
22+
## 核心概念
23+
24+
### 测试用例格式
25+
26+
每个测试用例最多 3 个文件(放在 `benchmarks/cases/`):
27+
```
28+
001_simple_add.dsl ← DSL 源代码(必需)
29+
001_simple_add.expected ← 期望输出(可选)
30+
001_simple_add.desc ← 简短描述(可选)
31+
```
32+
33+
### 23 个用例覆盖范围
34+
35+
| 类别 | 数量 | 示例 |
36+
|------|------|------|
37+
| 算术 | 5 | add, mul, sub, div, chained |
38+
| 神经网络 | 6 | relu, gelu, softmax, matmul, dot, maxpool |
39+
| 控制流 | 7 | for-loop, if/else, while, nested |
40+
| 复杂场景 | 3 | NN pipeline, large chain |
41+
| 常量 | 1 | constant propagation |
42+
43+
### 报告格式
44+
45+
| 格式 | 用途 |
46+
|------|------|
47+
| **终端输出** | 开发时快速查看 |
48+
| **JSON** | CI dashboard 数据源 |
49+
| **Markdown** | 文档、PR review |
50+
| **HTML** | 可视化展示 |
51+
52+
---
53+
54+
## 一步步
55+
56+
### 运行全部测试
57+
58+
```bash
59+
# 基础运行
60+
python benchmarks/bench_runner.py
61+
62+
# 指定用例目录
63+
python benchmarks/bench_runner.py benchmarks/cases
64+
65+
# 生成报告
66+
python benchmarks/bench_runner.py \
67+
--output-json benchmark_reports/dsl_bench.json \
68+
--output-html benchmark_reports/dsl_bench.html
69+
70+
# 性能基准模式(多次重复取平均)
71+
python benchmarks/bench_runner.py --repeat 3
72+
```
73+
74+
### 用 Makefile 一键运行
75+
76+
```bash
77+
make bench # 等价于上面的完整流程
78+
```
79+
80+
### 添加新测试用例
81+
82+
```bash
83+
cat > benchmarks/cases/024_my_test.dsl << 'EOF'
84+
a = const(10)
85+
b = const(20)
86+
c = add(a, b)
87+
return c
88+
EOF
89+
90+
echo "30" > benchmarks/cases/024_my_test.expected
91+
echo "My custom test: 10 + 20 = 30" > benchmarks/cases/024_my_test.desc
92+
93+
# 运行确认
94+
python benchmarks/bench_runner.py
95+
```
96+
97+
---
98+
99+
## 代码走读
100+
101+
### BenchmarkRunner 核心
102+
103+
```python
104+
class BenchmarkRunner:
105+
def discover_cases(self):
106+
"""扫描目录,找到所有 .dsl 文件"""
107+
cases = []
108+
for f in sorted(self.test_dir.glob("*.dsl")):
109+
name = f.stem
110+
expected = self.test_dir / f"{name}.expected"
111+
desc = self.test_dir / f"{name}.desc"
112+
cases.append({
113+
"name": name,
114+
"dsl_path": str(f),
115+
"expected_path": str(expected) if expected.exists() else None,
116+
"desc": desc.read_text().strip() if desc.exists() else "",
117+
})
118+
return cases
119+
120+
def run_case(self, case):
121+
"""编译 DSL → IR → 汇编 → 仿真执行 → 对比期望输出"""
122+
result = BenchmarkResult(name=case["name"])
123+
# 1. 解析 DSL
124+
t0 = time.perf_counter()
125+
program = self.parser.parse_file(case["dsl_path"])
126+
result.parse_time = time.perf_counter() - t0
127+
# 2. 编译
128+
machine_instrs = compile_pipeline(program)
129+
# 3. 仿真执行
130+
output = self.simulator.run(machine_instrs)
131+
# 4. 对比期望
132+
if case["expected_path"]:
133+
expected = Path(case["expected_path"]).read_text().strip()
134+
result.passed = (output.strip() == expected)
135+
return result
136+
```
137+
138+
### CI 回归检测
139+
140+
```python
141+
# 生成 baseline
142+
baseline = json.load(open("baseline.json"))
143+
current = json.load(open("current.json"))
144+
145+
if current["pass_rate"] < baseline["pass_rate"]:
146+
print("REGRESSION DETECTED!")
147+
exit(1)
148+
```
149+
150+
---
151+
152+
## 动手练习
153+
154+
### 练习 1: 添加一个新测试用例
155+
156+
为你正在开发的编译器功能写一个测试用例,跑通并确认通过。
157+
158+
### 练习 2: 故意引入 bug
159+
160+
修改代码引入一个 bug(比如把 `ADD` 映射成 `SUB`),跑 benchmark 看看哪些测试会失败。
161+
162+
### 练习 3: 分析性能趋势
163+
164+
连续跑 3 次 benchmark,对比 JSON 输出,看编译时间是否有波动。
165+
166+
---
167+
168+
## 常见坑
169+
170+
| 坑 | 说明 |
171+
|----|------|
172+
| **expected 文件格式** | .expected 文件应该是纯文本,末尾不要有多余换行 |
173+
| **超时** | 某些测试可能死循环,BenchmarkRunner 有 timeout 保护 |
174+
| **仿真器差异** | 不同仿真器的结果可能略有不同(尤其是浮点),expected 应基于同一仿真器 |
175+
176+
---
177+
178+
## 进阶阅读
179+
180+
- [03-指标解读指南](../03-指标解读指南.md) — 如何解读 benchmark 数据
181+
- pytest 文档:[pytest](https://docs.pytest.org/) — Python 测试框架
182+
- 相关 topic: [Topic 19 — Standalone RISC-V 编译器](19-Standalone-RISC-V编译器.md) | [Topic 30 — CI Dashboard](30-CI-Dashboard.md)
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
# Topic 14 — 常量加载合并优化
2+
3+
> **难度**: 中级 | **源文件**: `scratchv/backend/const_merge.py`
4+
5+
---
6+
7+
## 是什么?
8+
9+
常量加载合并(`const_merge.py`)在汇编层面优化 RISC-V 加载 32 位常量的指令序列。RISC-V 要加载一个 32 位常数需要两条指令(`lui` + `addi`),这个优化器做了两件事:
10+
11+
1. **lui+addi 合并**:把相邻的 `lui rd, hi` + `addi rd, rd, lo` 合并为一条 `li rd, full_value`
12+
2. **冗余 lui 消除**:删除连续两次加载相同高位立即数的 `lui`
13+
14+
---
15+
16+
## 为什么?
17+
18+
RISC-V 指令都是 32 位定长,无法在一条指令里编码 32 位立即数。所以加载常数需要:
19+
```asm
20+
lui t0, 0x12345 # t0 = 0x12345 << 12 = 0x12345000
21+
addi t0, t0, 0x678 # t0 = t0 + 0x678 = 0x12345678
22+
```
23+
24+
编译器简单翻译时可能产生不必要的冗余。这个优化器在汇编生成后做"post-pass"清理。
25+
26+
---
27+
28+
## 核心概念
29+
30+
### RISC-V 32 位常数加载
31+
32+
```
33+
最终值 = (imm_hi << 12) + sign_extend_12(imm_lo)
34+
```
35+
36+
关键细节:`addi` 的 12 位立即数会被**符号扩展**。`0x800` → `-2048`(不是 `+2048`)。
37+
38+
### 两遍优化
39+
40+
**Pass 1: lui+addi 合并**
41+
```
42+
lui t0, 0x12345 → li t0, 0x12345678
43+
addi t0, t0, 0x678
44+
```
45+
条件:两条指令相邻,目标寄存器相同,addi 源寄存器与 lui 目标相同。
46+
47+
**Pass 2: 冗余 lui 消除**
48+
```
49+
lui t0, 0x12345 → (删除)
50+
... (t0 未修改)
51+
lui t0, 0x12345 lui t0, 0x12345(第一次已加载)
52+
```
53+
54+
---
55+
56+
## 一步步
57+
58+
### 使用
59+
60+
```python
61+
from scratchv.backend.const_merge import merge_constants
62+
63+
asm = """
64+
lui t0, 0x12345
65+
addi t0, t0, 0x678
66+
lui t0, 0x12345
67+
addi t0, t0, 256
68+
"""
69+
70+
optimized, changes = merge_constants(asm)
71+
print(optimized)
72+
print(f"Changes: {changes}")
73+
```
74+
75+
### 命令行
76+
77+
```bash
78+
python -m scratchv.backend.const_merge input.s -o output.s -v
79+
```
80+
81+
---
82+
83+
## 代码走读
84+
85+
### 合并逻辑
86+
87+
```python
88+
def _merge_lui_addi(self, lines):
89+
"""找到相邻的 lui+addi 对并合并"""
90+
for i in range(len(lines) - 1):
91+
if lines[i].opcode == "lui" and lines[i+1].opcode == "addi":
92+
if lines[i].operands[0] == lines[i+1].operands[0]: # 同目标
93+
if lines[i+1].operands[1] == lines[i].operands[0]: # addi 读同一寄存器
94+
hi = int(lines[i].operands[1], 16) << 12
95+
lo = self._sign_extend_12(int(lines[i+1].operands[2]))
96+
full = (hi + lo) & 0xFFFFFFFF
97+
lines[i] = AsmLine(opcode="li", operands=[
98+
lines[i].operands[0], str(full)
99+
])
100+
lines[i+1] = None # 标记删除
101+
```
102+
103+
### 符号扩展处理
104+
105+
```python
106+
def _sign_extend_12(self, imm):
107+
if imm & 0x800: # 第11位是1 → 负数
108+
return imm - 0x1000
109+
return imm
110+
```
111+
112+
---
113+
114+
## 动手练习
115+
116+
### 练习 1: 观察合并效果
117+
118+
编译 CNN 模型,对输出汇编运行 `const_merge`,统计合并了多少对 lui+addi。
119+
120+
### 练习 2: 手动计算
121+
122+
手工计算 `lui t0, 0x12345` + `addi t0, t0, 0x800` 的最终值(注意符号扩展)。
123+
124+
### 练习 3: 扩展优化
125+
126+
添加对 `li` 后紧跟 `addi` 的合并(`li x, N; addi x, x, M` → `li x, N+M`)。
127+
128+
---
129+
130+
## 常见坑
131+
132+
| 坑 | 说明 |
133+
|----|------|
134+
| **符号扩展陷阱** | `addi` 的 12 位立即数会被符号扩展,`0x800` = `-2048`,不是 `+2048` |
135+
| **寄存器被修改** | 两次 `lui` 之间如果目标寄存器被其他指令修改了,就不能消除 |
136+
| **li 是伪指令** | `li rd, imm` 是汇编器伪指令,实际可能被展开为 `lui+addi` 或单条 `addi` |
137+
138+
---
139+
140+
## 进阶阅读
141+
142+
- RISC-V 指令集手册:第 2.4 节(Immediate Encoding)
143+
- 相关 topic: [Topic 13 — 窥孔优化器](13-窥孔优化器.md) | [Topic 12 — 指令计数统计器](12-指令计数统计器.md)

0 commit comments

Comments
 (0)