第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 性能分析核心流程 perf stat 全局统计 CPU 占用高? perf record 采样热点 perf stat -e 查缓存/分支 定位瓶颈函数/代码行 优化后再次验证

实战案例:优化一个字符串处理函数

我记得有一次,一个网络协议解析程序跑得很慢。用 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