4、perf实战:CPU热点分析:使用perf定位CPU密集型函数,优化循环和算法。

说到性能调优,我最常被问到的一个问题就是:「我的程序跑得慢,到底慢在哪?」

嗯,这个问题其实挺难回答的。没有数据,全靠猜,那跟算命没啥区别。我个人习惯,第一步就是上 perf 这个工具。它就像程序的CT机,能直接告诉你CPU的时间都花在哪些函数上了。

今天我们就来实战一把,看看怎么用perf定位CPU热点,然后优化那些拖后腿的循环和算法。

4.1 什么是CPU热点?

说白了,CPU热点就是程序里那些「吃CPU大户」的函数或代码段。它们占用了大量的CPU时间片,导致整体性能上不去。

你想想看,如果一个函数只占1%的CPU时间,你花三天去优化它,效果微乎其微。但如果一个函数占了80%的CPU时间,你稍微动一动,性能可能就翻倍了。这就是「二八定律」在性能优化里的体现。

核心原则: 优化热点,而不是优化一切。先找到那20%的代码,再动手。

4.2 perf的基本用法:采样与报告

perf的工作原理很简单——它每隔一段时间就打断CPU一次,记录下当前正在执行的函数。采样次数多了,就能统计出每个函数被命中的次数。命中率越高,说明这个函数越「热」。

4.2.1 准备一个「慢」程序

我们先写一个简单的C程序,里面有个明显的CPU密集型循环。我在项目中遇到过类似的情况,当时是一个图像处理算法,循环里做了大量无意义的计算。

// hot_loop.c
#include <stdio.h>

void slow_function() {
    int sum = 0;
    // 一个愚蠢的循环,纯属浪费CPU
    for (int i = 0; i < 100000000; i++) {
        sum += i * i / 3;
    }
    printf("sum = %d\n", sum);
}

void fast_function() {
    int sum = 0;
    // 稍微聪明一点的循环
    for (int i = 0; i < 100000000; i++) {
        sum += i;
    }
    printf("sum = %d\n", sum);
}

int main() {
    slow_function();
    fast_function();
    return 0;
}

编译时记得加 -g 选项,保留调试信息,这样perf才能定位到具体的代码行。

gcc -g -o hot_loop hot_loop.c

4.2.2 运行perf采样

执行以下命令,perf会以99Hz的频率采样(即每秒采样99次),持续到程序结束。

perf record -F 99 ./hot_loop

采样完成后,会生成一个 perf.data 文件。然后我们用 perf report 查看报告。

perf report

你会看到一个类似这样的界面:

Overhead Command Shared Object Symbol
72.34% hot_loop hot_loop slow_function
27.66% hot_loop hot_loop fast_function

看到了吗?slow_function 占了72%的CPU时间,而 fast_function 只占28%。这就是热点!

小技巧: 如果程序运行时间很短,可以加 -a 参数对所有CPU进行采样,或者用 perf stat 先看看整体性能指标。

4.3 深入热点:perf annotate 查看汇编

光知道哪个函数慢还不够,我们得知道函数里哪一行代码最慢。perf提供了 annotate 功能,可以直接把采样结果映射到源代码和汇编指令上。

perf annotate slow_function

输出会显示每一行代码的采样百分比。比如:

       │    for (int i = 0; i < 100000000; i++) {
  0.02 │      mov    -0x4(%rbp),%eax
       │      cmp    $0x5f5e100,%eax
 12.34 │    ↓ jl     10
       │        sum += i * i / 3;
 87.64 │      imul   %eax,%eax
       │      mov    %eax,%edx
       │      mov    $0x55555556,%eax
       │      imul   %edx
       │      mov    %edx,%eax
       │      shr    $0x1f,%eax
       │      add    %eax,%edx
       │      add    %edx,-0x8(%rbp)
       │      addl   $0x1,-0x4(%rbp)
       │    ↓ jmp    10

看到 87.64% 那行了吗?就是 sum += i * i / 3 这条语句。其中 imul(整数乘法)和后续的除法操作占了绝大部分开销。

为什么会这样?因为 i * i / 3 涉及乘法和除法,而除法是CPU里最慢的指令之一。相比之下,fast_function 里只有 sum += i,一个加法指令,快得多。

注意: 不要过度优化。有时候编译器会帮你做优化(比如用移位代替除法),但前提是你得写出「可优化」的代码。比如用 i * i * 0.333 可能反而更慢,因为涉及浮点运算。

4.4 优化实战:从算法层面下手

找到热点之后,怎么优化?我建议从两个角度考虑:

  1. 减少计算量:能不能用更简单的公式?
  2. 改变算法:能不能用空间换时间?

4.4.1 优化循环体

对于上面的例子,i * i / 3 其实可以改写。如果我们知道 i 的范围,可以预先计算好结果,用查表代替实时计算。

void optimized_function() {
    int sum = 0;
    // 查表法:预先计算 i*i/3 的结果
    static int table[100000000]; // 注意:实际中不要这样写,会爆栈
    for (int i = 0; i < 100000000; i++) {
        table[i] = i * i / 3;
    }
    for (int i = 0; i < 100000000; i++) {
        sum += table[i];
    }
    printf("sum = %d\n", sum);
}

当然,这个例子有点极端。实际项目中,查表法常用于三角函数、开方等复杂运算。我在做嵌入式音频处理时,就用查表法优化过 sin 函数,性能提升了5倍。

4.4.2 减少循环次数

有时候,循环本身的开销也不小。比如循环里有个 if 判断,能不能提到循环外面?

// 优化前
for (int i = 0; i < N; i++) {
    if (flag) {
        do_something(i);
    } else {
        do_other(i);
    }
}

// 优化后
if (flag) {
    for (int i = 0; i < N; i++) {
        do_something(i);
    }
} else {
    for (int i = 0; i < N; i++) {
        do_other(i);
    }
}

这样避免了每次循环都判断 flag,减少了分支预测失败的概率。

4.5 用perf验证优化效果

优化完,别急着高兴。我们得用数据说话。再次运行perf,看看优化后的热点分布。

perf record -F 99 ./hot_loop_optimized
perf report

如果优化有效,你会发现 slow_function 的占比大幅下降,甚至可能被 fast_function 反超。我曾经优化过一个网络协议栈,优化前一个函数占85%的CPU,优化后降到了30%。那种成就感,嗯,比写新功能还爽。

记住: 优化是一个迭代过程。每次改完,都要重新采样、分析、验证。不要凭感觉,要凭数据。

4.6 避坑指南

  • 不要优化非热点:我曾经花了一整天优化一个只占2% CPU的函数,结果整体性能几乎没变化。后来才意识到,那2%根本就不是瓶颈。
  • 注意编译器优化:有时候你写的代码,编译器在 -O2 下已经帮你优化掉了。用perf分析时,最好用 -O0 编译,或者对比不同优化级别的结果。
  • 采样频率别太高:-F 99 是常用值。如果设成 -F 9999,采样本身就会消耗大量CPU,影响结果准确性。
  • 多线程程序注意:perf默认只采样当前进程。对于多线程程序,可以用 -a 或 --per-thread 参数。

4.7 本章小结

perf的核心价值,就是帮你从「盲人摸象」变成「精准打击」。找到热点,分析热点,优化热点——这三步走下来,大部分性能问题都能解决。

我个人习惯,每次写完代码都会跑一遍perf,看看有没有「意外之喜」。有时候你会发现,自己以为很高效的代码,其实藏着不少坑。嗯,这就是工具的价值。

perf CPU热点分析流程 1. perf record 采样 记录CPU执行轨迹 2. perf report 分析 定位热点函数 3. perf annotate 深入 查看热点代码行 4. 优化循环/算法 减少计算量/查表 迭代验证 核心思想:用数据驱动优化,而不是靠直觉

可可精品资料库