性能调优基础: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 个根本没生效——因为硬件不支持。
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 支持两种主流采样方式:
- 周期采样(Periodic Sampling):每 N 个周期触发一次 PMU 中断,记录当前 PC(程序计数器)。
- 事件采样(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 计数器是硬件资源,数量有限,要精打细算
- 性能事件是调优的“探针”,选对事件比选多事件更重要
- 周期采样找热点,事件采样挖细节,两者互补
- 精确事件和模糊事件的区别,直接影响优化方向是否正确