4. 性能分析工具链:Linux perf 工具在 ARM64 上的使用
性能调优这件事,说白了就是回答三个问题:哪里慢?为什么慢?怎么改? 在 ARM64 平台上,Linux perf 就是回答前两个问题最趁手的家伙。我这些年调过的坑,十有八九都是靠 perf 先定位到的。
perf 是内核自带的性能剖析工具,不需要额外装什么商业软件。它利用 PMU(Performance Monitoring Unit)硬件计数器来采集数据,对目标程序的影响极小。这一点在 ARM64 上尤其重要——ARM 的 PMU 架构和 x86 有些差异,但 perf 已经帮你封装好了。
4.1 先确认你的环境
在敲 perf 命令之前,先检查三件事:
- 内核配置: 确认 CONFIG_PERF_EVENTS=y。大部分发行版默认开了,但嵌入式板子可能没开。
- PMU 可用性: 跑一下
perf list,看看能不能列出硬件事件。如果只看到软件事件,说明 PMU 没使能。 - 权限问题: 普通用户可能只能看自己的进程。加
sudo或者设置kernel.perf_event_paranoid = -1。
# 快速检查环境
uname -r
cat /proc/sys/kernel/perf_event_paranoid
perf list | grep -i "cache\|branch\|cycle" | head -20
4.2 perf stat:快速看全局
perf stat 是我用得最多的子命令。它跑完一个程序,然后告诉你关键计数器的汇总值。适合做「快照式」对比。
# 基本用法
perf stat ./my_arm64_app
# 指定事件
perf stat -e cycles,instructions,cache-references,cache-misses ./my_arm64_app
# 跑多次取平均
perf stat -r 5 ./my_arm64_app
输出长这样:
Performance counter stats for './my_arm64_app':
1,234,567,890 cycles # 2.4 GHz
987,654,321 instructions # 0.80 insn per cycle
12,345,678 cache-references # 1.0% of all cycles
1,234,567 cache-misses # 10.0% of all cache refs
0.512345678 seconds time elapsed
我最关注三个指标:
- IPC(Instructions Per Cycle): ARM64 上一般 0.5~1.5 算正常。低于 0.5 说明有严重瓶颈。
- Cache miss 率: 超过 5% 就该看看数据局部性了。
- 分支预测错误率: 超过 5% 说明分支模式混乱。
perf stat -r 3,把结果记下来。别信感觉,数据说话。
4.3 perf record & perf report:深入看热点
perf stat 只能看总量,想知道「哪行代码最慢」就得靠 perf record。它会采样程序执行时的 PC(程序计数器),然后生成报告。
# 采样 10 秒
perf record -F 99 -g ./my_arm64_app
# 生成报告
perf report
# 生成火焰图数据
perf script > out.perf
参数说明:
| 参数 | 含义 | 建议值 |
|---|---|---|
-F |
采样频率(Hz) | 99 或 199(避免与系统时钟共振) |
-g |
记录调用栈 | 必加,否则看不到函数关系 |
-p |
附加到已有进程 | 调试后台服务时用 |
--call-graph dwarf |
使用 DWARF 调试信息展开栈 | ARM64 上推荐,比 fp 模式更准 |
-g 出来的调用栈不完整,试试 --call-graph dwarf。我遇到过 GCC 优化后 fp 寄存器被复用的情况,换成 dwarf 就好了。
4.4 ARM64 特有的事件
ARM 的 PMU 有一些 x86 没有的事件,值得关注:
# 列出 ARM 架构特定事件
perf list | grep -i "arm\|arch\|imx\|exynos"
常见的有:
- stall_frontend / stall_backend: 前端(取指、解码)还是后端(执行、访存)在等?
- l1d_cache / l1i_cache: 数据 cache 和指令 cache 分开统计。
- br_mis_pred: 分支预测错误次数。
- bus_access: 总线访问次数,反映内存带宽压力。
stall_backend 很高,说明执行单元在等数据。这时候看 l1d_cache_miss 和 stall_ld_miss,基本能锁定是 cache 问题。
4.5 实战:定位一个 cache 性能问题
假设我们有这么一段代码:
// matrix_mult.c
void matrix_mult(int n, float *a, float *b, float *c) {
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++) {
float sum = 0.0f;
for (int k = 0; k < n; k++)
sum += a[i * n + k] * b[k * n + j];
c[i * n + j] = sum;
}
}
编译运行,用 perf 分析:
gcc -O2 -o matrix_mult matrix_mult.c
perf stat -e cycles,instructions,L1-dcache-load-misses ./matrix_mult
输出:
L1-dcache-load-misses: 45,678,901 (23.4% of all L1 loads)
23.4% 的 L1 miss 率,太高了。再用 perf record 看热点:
perf record -e L1-dcache-load-misses -g ./matrix_mult
perf report
报告显示热点在内层循环的 b[k * n + j] 访问。原因很明显:b 是按列访问的,而 C 语言数组按行存储,导致 cache 行利用率极低。
优化方案:交换 j 和 k 循环,或者对 b 做转置。优化后 L1 miss 率降到 2.1%,性能提升 3 倍。
4.6 知识结构图
下面这张图概括了 perf 工具链在 ARM64 上的使用逻辑:
4.7 常见问题与避坑
| 问题 | 现象 | 解决办法 |
|---|---|---|
| 采样频率不准 | 实际采样率远低于 -F 指定值 | 检查 /proc/sys/kernel/perf_event_max_sample_rate,默认可能只有 1000 |
| 调用栈不完整 | perf report 里很多 [unknown] | 加 --call-graph dwarf,或者编译时加 -fno-omit-frame-pointer |
| 事件计数为 0 | perf stat 显示某些硬件事件为 0 | 确认 PMU 是否使能,检查 dmesg | grep PMU |
| 多核调度干扰 | 结果每次跑都不一样 | 用 taskset -c 0 绑定到固定核心,或者 perf stat -a 看全系统 |
perf stat 报告的 cycles 比实际时间算出来的还少。查了半天,发现是固件把 PMU 的时钟源设成了低频备用时钟。这种硬件问题,perf 本身无能为力,得找板级支持包的人。
4.8 小结
perf 在 ARM64 上的用法和 x86 基本一致,但要注意三点:栈回溯方式、ARM 特有事件、PMU 硬件可用性。我个人建议的调试流程是:
- 先用
perf stat看 IPC 和 cache miss 率,判断瓶颈类型。 - 再用
perf record -g定位热点函数。 - 最后用
perf list找到对应 ARM 事件,做针对性优化。