课题25:LLVM vs ScratchV 缓存对比工具
难度:高 | 类型:参考分析 | 源文件:
scratchv/standalone/llvm_cache_compare.py状态:✅ 已完成
概述
llvm_cache_compare.py 是 ScratchV 的核心对比分析工具——它对 LLVM 生成的 RV64FD 汇编和 ScratchV 生成的 RV32IM 汇编做并排分析,计算两者在动态指令数、缓存行为、访存模式上的差异。这是 ScratchV 最重要的性能分析工具,告诉你"差距到底在哪里"。
理解背景
是什么?
llvm_cache_compare.py 对 LLVM 和 ScratchV 的汇编做并排分析。因为无法在 Python 内运行完整的 RV64FD 仿真,它使用分析估算法:
1. 从汇编中提取内层循环体的静态指令
2. 基于 CNN 层维度计算动态执行次数
3. 将访存模式输入 cache 模型估算命中率
4. 生成并排对比表
为什么?
这是 ScratchV 最重要的性能分析工具——它告诉你: - ScratchV 和 LLVM 的差距到底在哪里(哪个算子?哪类指令?) - 每次优化后的实际效果(量化对比) - Cache 层面的深层瓶颈(不仅仅是指令数)
核心概念
分析流程
LLVM .s 汇编 ScratchV .s 汇编
│ │
▼ ▼
提取内层循环体指令 提取内层循环体指令
(Conv/Gemm/MaxPool) (Conv/Gemm/MaxPool)
│ │
▼ ▼
按 CNN 层维度 × 循环次数 按 CNN 层维度 × 循环次数
→ 动态指令数估算 → 动态指令数估算
│ │
▼ ▼
Cache 模型 (I$/D$) Cache 模型 (I$/D$)
│ │
└──────────┬─────────────────────┘
▼
并排对比表 + JSON 输出
对比维度
LLVM .s 汇编 ScratchV .s 汇编
│ │
▼ ▼
提取内层循环体指令 提取内层循环体指令
(Conv/Gemm/MaxPool) (Conv/Gemm/MaxPool)
│ │
▼ ▼
按 CNN 层维度 × 循环次数 按 CNN 层维度 × 循环次数
→ 动态指令数估算 → 动态指令数估算
│ │
▼ ▼
Cache 模型 (I$/D$) Cache 模型 (I$/D$)
│ │
└──────────┬─────────────────────┘
▼
并排对比表 + JSON 输出
对比维度
| 维度 | LLVM (RV64FD) | ScratchV (RV32IM) |
|---|---|---|
| 动态 ALU 指令 | fmul/fadd (~2/MAC) | MUL+SRAI+ADD (~12/MAC) |
| 动态 Load/Store | 较少(寄存器多) | 较多(寄存器压力大) |
| 地址计算 | GEP(1条) | MUL+ADD 链(~5条) |
| I$ 行为 | 较好(指令紧凑) | 较差(指令多) |
| D$ 行为 | 接近 | 接近(数据访问模式相同) |
理解要点
- 理解分析估算法的原理:内层循环指令 × 循环迭代次数 = 动态指令数
- 掌握对比工具的完整分析流程(汇编→提取内层循环→估算动态→Cache 模型→并排对比)
- 能够独立运行对比工具并解读 JSON/Markdown 输出
- 理解 LLVM vs ScratchV 在各维度的差距根源(ALU 指令数差距最大,D$ 行为接近)
- 了解对比工具在 CI Dashboard 中的角色(数据源)
交付产物
- 一次完整的对比运行输出(JSON + Markdown 报告)
- 差距分析笔记(按算子、按指令类别的差距矩阵)
- 内层循环指令的逐条对比表(ScratchV vs LLVM)
代码走读
内层循环指令提取
def extract_inner_loop(asm_text, op_name):
"""从汇编中识别并提取指定算子的最内层循环指令"""
# 1. 找到算子标签(如 "conv2d_loop_ic:")
# 2. 提取标签到跳转之间的所有指令
# 3. 分类每条指令(ALU/MEM/BRANCH/FP)
# 4. 返回指令列表和分类统计
inner_loop = []
in_loop = False
for line in asm_text.split("\n"):
if f"{op_name}_loop_ic:" in line:
in_loop = True
continue
if in_loop:
if line.strip().startswith("j ") or line.strip().startswith("bne "):
break # 循环结束
inner_loop.append(parse_instruction(line))
return inner_loop
动态指令数估算
def estimate_dynamic(inner_loop_insns, layer_dims):
"""内层循环指令数 × 循环迭代次数 = 动态指令数"""
total = 0
for layer in layer_dims:
if layer.name.startswith("Conv"):
# 内层循环迭代次数 = OH × OW × OC × KH × KW × IC
iterations = layer.h_out * layer.w_out * ...
total += len(inner_loop_insns) * iterations
return total
动手练习
练习 1: 运行对比
- 一次完整的对比运行输出(JSON + Markdown 报告)
- 差距分析笔记(按算子、按指令类别的差距矩阵)
- 内层循环指令的逐条对比表(ScratchV vs LLVM)
代码走读
内层循环指令提取
def extract_inner_loop(asm_text, op_name):
"""从汇编中识别并提取指定算子的最内层循环指令"""
# 1. 找到算子标签(如 "conv2d_loop_ic:")
# 2. 提取标签到跳转之间的所有指令
# 3. 分类每条指令(ALU/MEM/BRANCH/FP)
# 4. 返回指令列表和分类统计
inner_loop = []
in_loop = False
for line in asm_text.split("\n"):
if f"{op_name}_loop_ic:" in line:
in_loop = True
continue
if in_loop:
if line.strip().startswith("j ") or line.strip().startswith("bne "):
break # 循环结束
inner_loop.append(parse_instruction(line))
return inner_loop
动态指令数估算
def estimate_dynamic(inner_loop_insns, layer_dims):
"""内层循环指令数 × 循环迭代次数 = 动态指令数"""
total = 0
for layer in layer_dims:
if layer.name.startswith("Conv"):
# 内层循环迭代次数 = OH × OW × OC × KH × KW × IC
iterations = layer.h_out * layer.w_out * ...
total += len(inner_loop_insns) * iterations
return total
动手练习
练习 1: 运行对比
def extract_inner_loop(asm_text, op_name):
"""从汇编中识别并提取指定算子的最内层循环指令"""
# 1. 找到算子标签(如 "conv2d_loop_ic:")
# 2. 提取标签到跳转之间的所有指令
# 3. 分类每条指令(ALU/MEM/BRANCH/FP)
# 4. 返回指令列表和分类统计
inner_loop = []
in_loop = False
for line in asm_text.split("\n"):
if f"{op_name}_loop_ic:" in line:
in_loop = True
continue
if in_loop:
if line.strip().startswith("j ") or line.strip().startswith("bne "):
break # 循环结束
inner_loop.append(parse_instruction(line))
return inner_loop
动态指令数估算
def estimate_dynamic(inner_loop_insns, layer_dims):
"""内层循环指令数 × 循环迭代次数 = 动态指令数"""
total = 0
for layer in layer_dims:
if layer.name.startswith("Conv"):
# 内层循环迭代次数 = OH × OW × OC × KH × KW × IC
iterations = layer.h_out * layer.w_out * ...
total += len(inner_loop_insns) * iterations
return total
动手练习
练习 1: 运行对比
def estimate_dynamic(inner_loop_insns, layer_dims):
"""内层循环指令数 × 循环迭代次数 = 动态指令数"""
total = 0
for layer in layer_dims:
if layer.name.startswith("Conv"):
# 内层循环迭代次数 = OH × OW × OC × KH × KW × IC
iterations = layer.h_out * layer.w_out * ...
total += len(inner_loop_insns) * iterations
return total
练习 1: 运行对比
运行 llvm_cache_compare.py,查看 JSON 输出,找出差距最大的指令类别。
练习 2: 添加新的对比维度
在对比脚本中添加"每 MAC 的 Load 指令数"指标,比较两种路径的数据复用效率。
练习 3: 对比不同模型
如果有多个 ONNX 模型(不同大小/层数),分别运行对比,看差距是否随模型规模变化。
常见坑
| 坑 | 说明 |
|---|---|
| LLVM 汇编依赖 | 需要先生成 LLVM 汇编(通过 llc),如果没安装 LLVM 工具链则无法对比 |
| 内层循环识别 | 依赖汇编中的特定标签命名(如 conv2d_loop_ic),如果代码生成器改了标签名,提取会失败 |
| Cache 模型参数 | 确保两边的 cache 参数一致(I$ 64:2:32, D$ 128:4:32),否则对比无意义 |
进阶阅读
- 03-指标解读指南 — 如何解读对比数据
- ARCHITECTURE — 双路径对比详解
- 相关 topic: 课题23 — Cache 模型 | 课题30 — CI Dashboard
自学路线
- 第 1 周:运行
llvm_cache_compare.py,生成 JSON 和 Markdown 报告。逐项理解每个对比维度(ALU/Load/Store/Branch/I$/D$)的含义和计算方式。
- 第 2 周:阅读
llvm_cache_compare.py 源码。理解 extract_inner_loop() 的标签识别逻辑和 estimate_dynamic() 的迭代次数计算。验证估算结果与 TinyFive 实测的偏差。
- 第 3 周:深入分析差距最大的维度和算子。是 Conv2D 的 MUL 指令太多?还是地址计算的 ADD 链太长?画出每个算子的指令类别分布饼图。
- 第 4 周:为对比工具添加一个新维度(如"每 MAC 的分支指令数"或"寄存器溢出次数估算")。将新指标集成到
dashboard.py 的 HTML 输出中。
- 第 1 周:运行
llvm_cache_compare.py,生成 JSON 和 Markdown 报告。逐项理解每个对比维度(ALU/Load/Store/Branch/I$/D$)的含义和计算方式。 - 第 2 周:阅读
llvm_cache_compare.py源码。理解extract_inner_loop()的标签识别逻辑和estimate_dynamic()的迭代次数计算。验证估算结果与 TinyFive 实测的偏差。 - 第 3 周:深入分析差距最大的维度和算子。是 Conv2D 的 MUL 指令太多?还是地址计算的 ADD 链太长?画出每个算子的指令类别分布饼图。
- 第 4 周:为对比工具添加一个新维度(如"每 MAC 的分支指令数"或"寄存器溢出次数估算")。将新指标集成到
dashboard.py的 HTML 输出中。