第90章 性能优化:如何用 perf 进行性能分析?
性能优化这事儿,说实话,很多嵌入式工程师容易忽略。我早年也犯过这个毛病——代码能跑就行,管它快不快呢?直到有一次,一个产品因为中断响应慢了200微秒,导致整个系统丢包,客户投诉电话直接打到了老板那里。嗯,从那以后,我就把性能分析当成了必修课。
今天要聊的 perf,是我个人最常用的性能分析工具。它不像那些商业工具那么贵,也不像自己瞎猜那么不靠谱。说白了,perf 就是 Linux 内核自带的一把手术刀,能帮你精准定位性能瓶颈。
perf 是什么?
perf 全称是 Performance Events Subsystem,它利用 CPU 内部的性能计数器来采样。你想想看,CPU 每执行一条指令、每发生一次缓存未命中、每产生一次分支预测错误,硬件都会记录。perf 就是把这些数据读出来,告诉你——你的程序到底慢在哪里。
我习惯把 perf 分成三个层次:
- 统计模式:看全局,比如每秒发生了多少次上下文切换
- 采样模式:看热点,比如哪个函数占用了最多的 CPU 时间
- 追踪模式:看流程,比如系统调用的完整调用链
核心思想:不要猜,要测。perf 给你的不是推测,是硬件级别的真实数据。
安装与基本用法
大多数 Linux 发行版都自带 perf。如果没有,装一下也很简单:
# Ubuntu/Debian
sudo apt install linux-tools-common linux-tools-$(uname -r)
# CentOS/RHEL
sudo yum install perf
装完之后,先跑个最简单的命令试试:
perf stat ls
你会看到类似这样的输出:
Performance counter stats for 'ls':
1.234567 task-clock (msec) # 0.456 CPUs utilized
12 context-switches # 0.010 M/sec
0 cpu-migrations # 0.000 K/sec
345 page-faults # 0.279 M/sec
3,456,789 cycles # 2.800 GHz
2,123,456 instructions # 0.61 insn per cycle
456,789 branches # 370.123 M/sec
12,345 branch-misses # 2.70% of all branches
看到没?一条命令,CPU 利用率、上下文切换次数、指令数、分支预测错误率全出来了。我当年要是早点学会这个,也不至于为了一个循环优化折腾三天。
实战:定位 CPU 热点
假设你有一个程序 myapp,跑起来很慢。你想知道到底是哪个函数在吃 CPU。用 perf 的采样模式:
perf record -g ./myapp
perf report
-g 参数表示记录调用栈。跑完之后,perf report 会打开一个交互界面,按 CPU 占用率从高到低列出函数。你会看到类似:
52.34% myapp myapp [.] process_data
23.45% myapp myapp [.] hash_calculate
10.12% myapp libc.so [.] __memcpy_avx_unaligned
5.67% myapp myapp [.] validate_input
一目了然。process_data 占了 52% 的 CPU 时间,这就是你要优化的重点。
我的习惯:先看最上面的三个热点。如果第一个函数就占了 50% 以上,那优化它一个就够了。如果热点分散,说明问题可能出在架构层面,比如缓存设计不合理。
避坑指南:采样频率与精度
我曾经犯过一个低级错误——采样频率设得太高,结果 perf 自己占用了大量 CPU,导致分析结果失真。后来我学乖了:
- 默认采样频率是 1000 Hz(每秒 1000 次),对大多数场景足够了
- 如果程序运行时间很短,可以调高到 4000 Hz:
perf record -F 4000 ./myapp - 如果程序跑几个小时,建议降到 100 Hz,避免采样开销影响结果
注意:perf 采样是统计性的,不是精确的。它告诉你的是「大概有 52% 的采样点落在了 process_data 函数里」,而不是「process_data 精确占用了 52.000% 的 CPU」。别把小数点当真。
高级用法:缓存命中与分支预测
有时候 CPU 占用率不高,但程序就是慢。这时候问题可能出在缓存未命中或者分支预测失败上。perf 可以帮你查:
perf stat -e cache-misses,cache-references,branch-misses,branches ./myapp
输出示例:
12,345,678 cache-references
3,456,789 cache-misses # 28.0% of all cache refs
5,678,901 branches
567,890 branch-misses # 10.0% of all branches
缓存未命中率 28%?这太高了。我见过一个项目,就是因为数据结构设计不合理,导致频繁缓存未命中,性能直接腰斩。优化方案很简单——把结构体成员按访问频率重新排列,把热点数据放在同一缓存行里。
知识体系:perf 的核心逻辑
下面这张图是我自己总结的 perf 分析流程,你照着走一遍,基本不会漏掉关键问题:
实战案例:优化一个字符串处理函数
我记得有一次,一个网络协议解析程序跑得很慢。用 perf record 一抓,发现 strlen 被调用了上百万次。代码大概是这样的:
void parse_message(const char *msg) {
size_t len = strlen(msg); // 每次都要遍历整个字符串
// ... 处理逻辑
}
问题在哪?每次调用 parse_message 都要重新计算字符串长度。如果消息很长,这个开销就非常可观。优化方案:
void parse_message(const char *msg, size_t len) {
// 长度由调用者传入,避免重复计算
// ... 处理逻辑
}
改完之后,用 perf stat 对比:
| 指标 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| CPU 占用 | 85% | 32% | 62% |
| 指令数 | 12.5 亿 | 4.2 亿 | 66% |
| 缓存未命中 | 8.3% | 3.1% | 63% |
你看,一个简单的改动,性能提升立竿见影。这就是 perf 的价值——它告诉你该往哪个方向使劲。
总结
perf 这个工具,说复杂也复杂,说简单也简单。我个人觉得,掌握三个命令就够了:
perf stat:看全局,快速判断是不是 CPU 密集型问题perf record/report:看热点,精准定位瓶颈函数perf stat -e:看细节,查缓存、分支、内存访问
别想着一次把所有功能都学会。先跑起来,遇到问题再查文档。我当年就是这么过来的——先会用,再精通。
最后提醒一句:性能优化不是玄学。有了 perf,你就不再需要靠猜了。数据会告诉你答案。
公众号:蓝海资料掘金营,微信deep3321