跳转至

性能分析与优化

性能分析的目标不是“找到一个看起来相关的计数器”,而是构造一条证据链:

稳定的 workload
  → 可复现的性能差异
  → Top-down 定位大类
  → 模块事件验证机制
  → 源码/波形确认因果
  → 单变量修改与回归

从三个基准数开始

任何实验先记录:

IPC = minstret / mcycle
CPI = mcycle / minstret
有效工作比例 = 退休指令或目标操作 / 总周期

minstret 是退休的架构指令数,不等于内部微操作数、发射数或执行数。向量拆分、 融合、错误路径执行和 replay 都会让这些口径不同。

香山的事件路径

源码中大量模块使用 XSPerfAccumulate 记录事件,并通过 generatePerfEvent 形成性能事件集合。PFEventmhpmevent3 起的事件选择 CSR 提供状态, HPM counter 再对选中事件计数。

关键入口:

事件编号不是跨版本 ABI

模块参数、事件拼接顺序和配置可能改变事件映射。不要把一次生成中的 raw event ID 永久写进脚本;应保存对应 commit、CONFIG 和该次生成的事件清单。

Top-down:先分大类

当前源码有两类互补的归因信息:

  1. TopDownCounters:前端 bubble、ROB/LQ/SQ、freelist、dispatch 等更细的 stall reason;
  2. TopDownGen:用 issued uops、LQ/SQ 状态与 L1/L2/L3 miss 信号形成执行和 内存 stall 事件。

例如 TopDownGen 中的内存层级归因是逐层收窄:

无 uop 发射且 LQ 非空
  └── 有 L1 miss
      └── 同时有 L2 miss
          └── 同时有 L3 miss

实现见 TopDownGen.scala#L13-L52。 这是一种实现定义的归因启发式,不等价于“该层 miss 独占造成了全部停顿周期”; 仍需结合队列占用、依赖链和实验验证。

从现象到结构

现象 第一假设 需要交叉验证
前端 bubble 高 BPU 错误、ICache/ITLB miss、取指碎片 各类 redirect、FTQ/IBuffer 占用、代码布局
ROB 经常满 头部有长延迟操作,退休被堵住 ROB head 类型、cache/TLB miss、div/FP/vector
IQ 满但发射少 操作数依赖或端口/FU 冲突 ready 位、wakeup、FU busy、写回冲突
LQ/SQ 满 miss/replay/commit drain 不足 MSHR、前递、违例、SBuffer、fence
L1 miss 高但 IPC 尚可 MLP/预取隐藏了延迟 outstanding miss、prefetch accuracy、ROB head
L1 miss 不高但 load 慢 TLB、bank conflict、前递或 replay TLB event、DCache bank、LSQ replay 原因
分支数不多却前端慢 少量高代价、难预测分支 每千指令 mispredict 与单次恢复代价
多核锁竞争严重 cache line ping-pong RFO/ownership、snoop、store/fence、NUMA/拓扑

仓库自带 Top-down 工具

scripts/top-down 可以读取 checkpoint 的统计输出,按前端、后端、访存和自定义 维度生成 CSV 与图表。主入口:

cd scripts/top-down
python3 top_down.py \
  --base-stat-dir <base_perf_report> \
  --json <checkpoint.json>

对比两个版本时必须提供相应 issue width:

python3 top_down.py \
  --base-stat-dir <base> \
  --ref-stat-dir <ref> \
  --json <checkpoint.json> \
  --base-issue 8 \
  --ref-issue 8 \
  --base-label Base \
  --ref-label Change

以实际生成配置为准,不要机械照抄示例中的宽度。完整参数见 scripts/top-down/README.md

一套可复用的分析流程

1. 固定实验

  • 固定 commit、config、频率/仿真模型、编译器和 workload 输入;
  • 预热 cache/分支预测器或明确冷启动口径;
  • 至少重复多次,报告中位数与离散度;
  • 同时保留 raw counter,避免只剩归一化图。

2. 用差值而不是绝对值

比较 baseline 与 change:

Δcycles
Δinstructions
Δstall_category
event_per_kinst = 1000 × event / instructions
Δevent_per_kinst = change_event_per_kinst - base_event_per_kinst

总量、每千指令事件和每周期事件回答不同问题。代码变更若改变动态指令数,只看 raw miss 数通常会误判。

3. 构造可证伪假设

差的表述:

IPC 下降可能和 cache 有关。

好的表述:

这次布局修改提高了同一热循环的 ICache miss/kinst;若它是主因,则恢复旧布局 应同时降低 ICache miss、前端 bubble 和 cycles,而退休指令数基本不变。

4. 回到源码

找到事件的产生条件,确认:

  • 是 pulse 计次、周期占用,还是 PopCount 增量;
  • 是否包含错误路径、预取、replay 或多个端口;
  • enable/disable 条件和 counter 位宽;
  • 事件名称到 HPM 选择值的生成顺序。

5. 只改一个变量

用代码布局、预取开关、数据集大小、依赖链长度或并发线程数构造对照。若无法让 目标结构的压力单调变化,就还没有真正验证归因。

优化优先级

通常按“损失周期 × 可改善比例 × 风险”排序,而不是按某个事件的绝对值排序。 高 miss rate 若已被 MLP 完全隐藏,未必比一个低频但每次清空大窗口的错误预测 更值得优先处理。