性能调优基础:PMU计数器、性能事件、采样方法

性能调优这件事,说白了就是回答三个问题:系统到底慢在哪?为什么慢?怎么改? 在 ARMv9 平台上,PMU(Performance Monitoring Unit)就是我们回答这些问题的“听诊器”。

我个人习惯,拿到一个新平台,第一件事不是跑 benchmark,而是先看看 PMU 能不能正常工作。你想想看,如果连“听诊器”都是坏的,后面的调优根本无从谈起。

2.1 PMU 计数器:硬件给你的一手数据

ARMv9 的 PMU 继承自 ARMv8,但做了不少增强。核心部件就是一组硬件计数器,它们直接焊在 CPU 核里,每个时钟周期都在默默累加。

计数器类型 数量(典型值) 说明
固定周期计数器 1 个(PMCCNTR) 每个时钟周期自动加 1,用于测量时间
可编程事件计数器 6~8 个(PMEVCNTRn) 可配置监控不同性能事件
扩展计数器(v8.5+) 最多 31 个 需要 FEAT_PMUv3p5 支持

嗯,这里要注意:可编程计数器是稀缺资源。ARMv9 标准实现通常只有 6 个,高端核可能有 8 个。我在项目中遇到过,有人一口气配了 10 个事件,结果发现后面 4 个根本没生效——因为硬件不支持。

避坑指南: 我曾经在调试一个网络加速卡时,发现 PMU 读数总是跳变。查了两天才找到原因——计数器溢出。默认情况下,32 位计数器在 2GHz 主频下,大约 2 秒就溢出了。如果你不处理溢出中断,读到的全是垃圾数据。

2.2 性能事件:你到底想看什么?

ARM 定义了一套标准性能事件,每个事件都有一个事件号(Event Number)。你往 PMEVTYPERn 寄存器里写事件号,对应的 PMEVCNTRn 就开始计数。

我个人最常用的几个事件:

  • 0x11 — CPU_CYCLES:周期数,最基础的计时单位
  • 0x08 — INST_RETIRED:退休指令数,看吞吐量
  • 0x1B — L1D_CACHE_REFILL:L1 数据缓存缺失,内存瓶颈的早期信号
  • 0x1C — L1D_CACHE:L1 数据缓存访问总次数
  • 0x70 — STALL_FRONTEND:前端停顿,指令取指跟不上
  • 0x71 — STALL_BACKEND:后端停顿,执行单元在等数据

你可能会问:事件这么多,我该选哪些?我的经验是:先看 CPI(Cycles Per Instruction)。CPI 高了,再往下拆是前端停顿还是后端停顿。后端停顿高,再查缓存缺失率。一层层剥洋葱。

核心公式:
CPI = CPU_CYCLES / INST_RETIRED
理想值接近 1.0,超过 2.0 说明有严重瓶颈。

2.3 采样方法:从统计到定位

光看总数不够。比如你发现 CPI=2.5,但不知道是哪个函数拖了后腿。这时候就需要采样。

ARMv9 支持两种主流采样方式:

  1. 周期采样(Periodic Sampling):每 N 个周期触发一次 PMU 中断,记录当前 PC(程序计数器)。
  2. 事件采样(Event-based Sampling):每发生 M 次指定事件触发一次中断,比如每 1000 次缓存缺失采样一次。

我记得在优化一个视频编解码库时,用周期采样发现 70% 的热点都在 memcpy 上。但换成事件采样后,发现真正的瓶颈是 cache miss 集中在某个结构体访问上——周期采样根本看不出来。

我的建议: 先用周期采样找热点函数,再用事件采样深挖热点函数内部的微架构问题。两个方法配合使用,效果最好。

2.4 采样中的陷阱:精确事件 vs 模糊事件

ARMv9 引入了一个重要概念——精确事件(Precise Event)。简单说,就是触发采样时,PMU 能准确告诉你“是哪条指令导致了事件”。

但并不是所有事件都支持精确采样。比如 L2 缓存缺失,在有些核上只能告诉你“大概在这个区域”,偏差可能达到几十条指令。这在做 PGO(Profile-Guided Optimization)时非常致命。

事件类型 精确度 典型用途
INST_RETIRED 精确 热点定位、PGO
BR_MIS_PRED 精确 分支预测优化
L1D_CACHE_REFILL 近似 缓存行为分析
L2D_CACHE_REFILL 模糊 仅做趋势判断

我曾经踩过一个坑:用 L2 缺失事件做采样,然后根据 PC 分布去优化代码。结果优化完性能反而下降了——因为采样的 PC 位置根本不对,我改错了地方。

2.5 实战:用 PMU 快速定位瓶颈

下面是一个简化版的 PMU 初始化代码,我习惯把它做成一个工具函数,每次调优时直接调用。

// 初始化 PMU 并配置两个计数器
void pmu_init(void) {
    // 1. 启用 PMU
    uint64_t pmcr = read_sysreg(PMCR_EL0);
    pmcr |= (1 << 0);  // E 位:启用
    pmcr |= (1 << 2);  // C 位:时钟计数复位
    write_sysreg(pmcr, PMCR_EL0);

    // 2. 清除溢出状态
    write_sysreg(0xFFFFFFFF, PMOVSCLR_EL0);

    // 3. 配置计数器 0 监控周期数
    write_sysreg(0x11, PMEVTYPER0_EL0);  // 0x11 = CPU_CYCLES

    // 4. 配置计数器 1 监控退休指令数
    write_sysreg(0x08, PMEVTYPER1_EL0);  // 0x08 = INST_RETIRED

    // 5. 启用两个计数器
    uint64_t cntenset = read_sysreg(PMCNTENSET_EL0);
    cntenset |= (1 << 0) | (1 << 1);
    write_sysreg(cntenset, PMCNTENSET_EL0);
}

嗯,这里要注意:不同厂商的 SoC 可能对 PMU 有修改。比如某些芯片会隐藏部分计数器,或者增加自定义事件。我建议你调优前先读一下芯片的 TRM(Technical Reference Manual),别全信 ARM 架构手册。

2.6 知识体系总览

下面这张图概括了本章的核心逻辑:从硬件计数器到性能事件,再到采样方法,最后落地到实际调优。

PMU 性能调优知识体系 PMU 硬件计数器 性能事件定义 采样方法 固定/可编程/扩展 标准事件 + 自定义事件 周期采样 / 事件采样 CPI / IPC 缓存缺失率 / 分支预测率 精确事件 vs 模糊事件 热点定位 → 瓶颈分析 → 代码优化 → 验证迭代 调优不是一次性的,而是一个“测量 → 分析 → 修改 → 再测量”的闭环
本章要点回顾:
  • PMU 计数器是硬件资源,数量有限,要精打细算
  • 性能事件是调优的“探针”,选对事件比选多事件更重要
  • 周期采样找热点,事件采样挖细节,两者互补
  • 精确事件和模糊事件的区别,直接影响优化方向是否正确