Simpleperf:CPU采样与硬件计数器分析

我刚做性能优化那会儿,工具链里最让我头疼的就是 CPU 热点定位。系统自带的 top 只能看个大概,tracepoint 挂多了又影响运行时。直到我认真啃下了 Simpleperf,才发现原来很多「玄学卡顿」都能被它扒得明明白白。

Simpleperf 是 Android 原生自带的采样分析器,说白了就是内核 perf 的封装。它不需要 root,不需要改代码,甚至可以在非 debug 版本上跑。我个人习惯把它当作性能优化的第一把刀——先用它扫一遍,再决定要不要上更重的工具。

CPU采样:从「猜」到「看」

CPU 采样的原理很简单:每隔一段时间拍一张快照,记录当前 CPU 正在执行的函数地址。样本多了,就能统计出每个函数的占比。我遇到过最典型的场景:一个视频播放页面滑动掉帧,开发者怀疑是解码线程的问题。结果用 Simpleperf 一跑,发现最热的函数竟然是 Layout 里的一个冗余循环。

采样频率怎么选? 我一般先用 1000 Hz 扫 10 秒,如果热点不明显,再降到 500 Hz 延长到 30 秒。频率太高会增加系统开销,低一些反而能抓到微卡顿。

先跑一个最简单的采样命令:

# 采样 10 秒,频率 1000 Hz
simpleperf record -p $(pid com.example.app) -f 1000 --duration 10 -o perf.data

采样完成后,用 report 命令生成可读报告:

simpleperf report -i perf.data --sort dso,symbol --percent 1

你会看到一个按函数占比排序的列表。占比超过 5% 的函数都值得仔细看看。我最常关注的是:

  • libc / libart —— 内存分配或 GC 有关?
  • 自定义 native 函数 —— 算法效率是否偏低?
  • FrameWork 层函数 —— 比如 View.draw 开销异常。

硬件计数器:不止是时间

CPU 采样只能告诉你「哪个函数占了多少时间」,但有些性能问题跟时间无关。比如缓存未命中、分支预测失败、内存带宽瓶颈——这些需要用硬件计数器来测量。

Simpleperf 通过 stat 命令直接读取 CPU 的 PMU 计数器。举个例子,我想知道 app 主线程的 L1 缓存缺失率:

simpleperf stat -p $(pid) -e L1-dcache-load-misses,L1-dcache-loads --duration 5

输出会给出两个事件的计数和比例。我遇到过一个问题:某个图片处理 native 库效率极低,用 CPU 采样看不出异常(因为它大部分时间在等内存),但 L1-dcache-load-misses 占比高达 40% —— 这才发现是图片解码时随机访问了不连续内存。

常用硬件计数器速查
事件名含义我通常关注阈值
cpu-cyclesCPU 时钟周期数与指令数对比判断 CPI
instructions已执行指令数IPC < 1 时大概率有瓶颈
L1-dcache-load-missesL1 数据缓存未命中超过 10% 值得深挖
branch-misses分支预测失败超过 5% 会明显拖慢性能
LLC-load-misses最后一级缓存未命中通常意味着内存访问模式有问题
核心观点: CPU 采样适合「找热点」,硬件计数器适合「分析热点为什么热」。两者搭配,基本能覆盖 90% 的 CPU 性能问题。

实战:从采样到定位的完整流程

我习惯按这个步骤来:

  1. 先采样:simpleperf record 抓 10~30 秒的 hot 区域。
  2. 看报告:report 找出 top 5 函数。
  3. 用硬件计数器验证:对每个热门函数所在进程,跑 stat 看缓存/分支/CPI。
  4. 关联源码:通过 simpleperf report --add-objdump 反汇编查看具体指令。
  5. 对比优化前后:同样的命令再跑一次,看占比下降了多少。
曾经有一个坑: 我在一个多核设备上采样,record 默认只采当前 CPU,结果热点函数在另一个核上完全没被捕获。解决办法是加 -a 参数采样所有核心:simpleperf record -a -f 1000。

下面是整个流程的示意图,我把它贴在组里的白板上,每次做优化前都看一眼:

Simpleperf CPU性能分析流程 Simpleperf 性能分析核心流程 采集原始数据 CPU 采样 (record) 硬件计数器 (stat) 生成报告 (report) 分析热点 & 定位瓶颈 优化 & 验证 虚线箭头表示「一次优化后回到第一步重新采集」,直到热点消除

几个实用小贴士

  • 采样期间最好让被测场景「匀速跑」,不要忽快忽慢,否则样本分布会失真。
  • 用 simpleperf record --trace-offcpu 还能捕获「线程在等什么」——比如被 binder 调用阻塞了。
  • 如果目标进程是 Java/Kotlin,记得加 --mmap-pages 4096,否则某些 JIT 编译的代码采样不到。
一句话总结: Simpleperf 既能「快照」又能「计数」,是 Android 性能优化工具箱里的瑞士军刀。花半天把它的常用命令摸透,以后排查线上问题能省下数倍时间。