Simpleperf:CPU采样与硬件计数器分析
我刚做性能优化那会儿,工具链里最让我头疼的就是 CPU 热点定位。系统自带的 top 只能看个大概,tracepoint 挂多了又影响运行时。直到我认真啃下了 Simpleperf,才发现原来很多「玄学卡顿」都能被它扒得明明白白。
Simpleperf 是 Android 原生自带的采样分析器,说白了就是内核 perf 的封装。它不需要 root,不需要改代码,甚至可以在非 debug 版本上跑。我个人习惯把它当作性能优化的第一把刀——先用它扫一遍,再决定要不要上更重的工具。
CPU采样:从「猜」到「看」
CPU 采样的原理很简单:每隔一段时间拍一张快照,记录当前 CPU 正在执行的函数地址。样本多了,就能统计出每个函数的占比。我遇到过最典型的场景:一个视频播放页面滑动掉帧,开发者怀疑是解码线程的问题。结果用 Simpleperf 一跑,发现最热的函数竟然是 Layout 里的一个冗余循环。
先跑一个最简单的采样命令:
# 采样 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-cycles | CPU 时钟周期数 | 与指令数对比判断 CPI |
| instructions | 已执行指令数 | IPC < 1 时大概率有瓶颈 |
| L1-dcache-load-misses | L1 数据缓存未命中 | 超过 10% 值得深挖 |
| branch-misses | 分支预测失败 | 超过 5% 会明显拖慢性能 |
| LLC-load-misses | 最后一级缓存未命中 | 通常意味着内存访问模式有问题 |
实战:从采样到定位的完整流程
我习惯按这个步骤来:
- 先采样:
simpleperf record抓 10~30 秒的 hot 区域。 - 看报告:
report找出 top 5 函数。 - 用硬件计数器验证:对每个热门函数所在进程,跑
stat看缓存/分支/CPI。 - 关联源码:通过
simpleperf report --add-objdump反汇编查看具体指令。 - 对比优化前后:同样的命令再跑一次,看占比下降了多少。
record 默认只采当前 CPU,结果热点函数在另一个核上完全没被捕获。解决办法是加 -a 参数采样所有核心:simpleperf record -a -f 1000。
下面是整个流程的示意图,我把它贴在组里的白板上,每次做优化前都看一眼:
几个实用小贴士
- 采样期间最好让被测场景「匀速跑」,不要忽快忽慢,否则样本分布会失真。
- 用
simpleperf record --trace-offcpu还能捕获「线程在等什么」——比如被 binder 调用阻塞了。 - 如果目标进程是 Java/Kotlin,记得加
--mmap-pages 4096,否则某些 JIT 编译的代码采样不到。