C++ `perf` 工具链深入:`perf stat`, `perf record`, `perf report` 分析 C++ 程序

哈喽,各位好!今天我们要聊聊C++程序员的秘密武器——perf工具链,特别是perf statperf recordperf report这三个神兵利器。别害怕,虽然名字听起来有点高冷,但用起来绝对让你欲罢不能。

一、perf stat: 一览众山小,摸清程序脉搏

想象一下,你是一个医生,程序是你的病人,perf stat就是你的听诊器。它能告诉你程序的心跳(CPU周期)、呼吸(指令数)、血液循环(缓存命中率)等等关键指标。

基本用法:

perf stat ./your_program

简单吧?运行后,你会看到类似这样的输出:

 Performance counter stats for './your_program':

          3.822342      seconds time elapsed

          3.787862      seconds user
          0.034297      seconds sys

     1,500,000,000      cycles                    #    0.392 GHz                      (scaled)
     2,000,000,000      instructions              #    1.333 IPC                      (scaled)
       500,000,000      cache-references          #  130.790 M/sec                    (scaled)
       400,000,000      cache-misses              #   26.761 M/sec                    (scaled)
         1,000,000      branches                  #  261.602 M/sec                    (scaled)
           500,000      branch-misses             #    1.911 %                       (scaled)

       3.822342489 seconds time elapsed

解读关键指标:

  • cycles: CPU周期数。越高,说明程序消耗的CPU资源越多。
  • instructions: 指令数。越高,说明程序执行的指令越多。
  • IPC (Instructions Per Cycle): 每周期指令数。越高,说明CPU的利用率越高,效率越高。通常认为IPC大于1比较理想。
  • cache-references: 缓存引用次数。CPU尝试从缓存中读取数据的次数。
  • cache-misses: 缓存未命中次数。CPU从缓存中找不到数据,不得不从内存中读取的次数。这个数字越高,说明缓存效率越低,程序性能可能存在瓶颈。
  • branches: 分支指令数。程序中ifelseswitch等分支语句的数量。
  • branch-misses: 分支预测失败次数。CPU预测错误分支的次数。分支预测失败会导致流水线阻塞,降低性能。

进阶用法:

  1. 指定事件:

    你可以指定perf stat监测的事件。例如,只想看缓存相关的统计:

    perf stat -e cache-references,cache-misses ./your_program

    perf list可以查看所有支持的事件。事件种类繁多,包括硬件事件(如CPU周期、缓存命中)和软件事件(如上下文切换、页面错误)。

  2. 指定时间间隔:

    -I选项可以指定统计的时间间隔(毫秒):

    perf stat -I 1000 ./your_program

    这会每隔1秒输出一次统计结果,方便你观察程序运行过程中性能的变化。

  3. 统计子进程:

    -c选项可以统计子进程的性能:

    perf stat -c ./your_program

    如果你的程序会创建子进程,这个选项很有用。

  4. 前后台运行,指定文件输出:

    perf stat的输出保存到文件,方便以后分析:

    perf stat -o perf.data ./your_program &

    这里使用了后台运行&,避免阻塞终端。

示例代码:

假设我们有这样一个C++程序 example.cpp

#include <iostream>
#include <vector>
#include <numeric>

int main() {
    std::vector<int> data(1000000);
    std::iota(data.begin(), data.end(), 0); // 填充数据 0, 1, 2, ...

    long long sum = 0;
    for (int i = 0; i < data.size(); ++i) {
        sum += data[i];
    }

    std::cout << "Sum: " << sum << std::endl;

    return 0;
}

编译:

g++ example.cpp -o example -O0  # 编译,关闭优化

运行并使用perf stat分析:

perf stat ./example

然后,我们开启优化重新编译:

g++ example.cpp -o example -O3  # 编译,开启最高级别优化

再次运行并使用perf stat分析。对比两次运行的输出,你会发现开启优化后,cyclesinstructions等指标都显著降低,IPC显著升高,程序性能得到了提升。

表格总结perf stat常用选项:

选项 描述
-e event 指定要监测的事件,如cyclescache-misses等。
-I ms 指定统计的时间间隔,单位为毫秒。
-c 统计子进程的性能。
-o file 将输出保存到文件。
-p pid 监测指定进程ID的性能。
-a 监测所有CPU的性能。
-g 启用调用图收集 (需要配合perf record)。

二、perf record: 录制程序运行轨迹,还原犯罪现场

perf stat告诉你程序整体的性能状况,但具体是哪个函数、哪行代码导致的性能瓶颈呢?这时候就需要perf record出马了。它能记录程序运行时的各种事件,生成一个数据文件,就像一个黑匣子,记录了程序的飞行轨迹。

基本用法:

perf record ./your_program

运行后,会在当前目录下生成一个perf.data文件。

进阶用法:

  1. 指定事件:

    perf stat一样,你可以指定perf record记录的事件:

    perf record -e cycles,cache-misses ./your_program
  2. 指定采样频率:

    -F选项可以指定采样频率(每秒多少次)。采样频率越高,记录的数据越详细,但也会带来更大的开销:

    perf record -F 99 ./your_program

    -F 99表示每秒采样99次。

  3. 收集调用栈:

    -g选项可以收集调用栈信息,这对于分析性能瓶颈非常有帮助。

    perf record -g ./your_program

    注意:使用-g选项需要程序包含调试信息(编译时不要strip)。

  4. 只记录用户空间的事件:

    -u选项可以只记录用户空间的事件,忽略内核空间的事件。这可以减少数据量,提高分析效率:

    perf record -u ./your_program

示例代码:

修改之前的 example.cpp,增加一个性能敏感的函数:

#include <iostream>
#include <vector>
#include <numeric>

// 一个耗时的函数
long long slow_function(const std::vector<int>& data) {
    long long sum = 0;
    for (int x : data) {
        // 模拟一些计算
        sum += (x * x * x) % 1000;
    }
    return sum;
}

int main() {
    std::vector<int> data(1000000);
    std::iota(data.begin(), data.end(), 0);

    long long sum = 0;
    for (int i = 0; i < data.size(); ++i) {
        sum += data[i];
    }

    // 调用耗时函数
    long long slow_sum = slow_function(data);
    std::cout << "Slow Sum: " << slow_sum << std::endl;
    std::cout << "Sum: " << sum << std::endl;

    return 0;
}

编译时要保留调试信息:

g++ example.cpp -o example -g -O0 # 编译,保留调试信息,关闭优化

使用perf record记录程序运行轨迹:

perf record -g ./example

表格总结perf record常用选项:

选项 描述
-e event 指定要记录的事件,如cyclescache-misses等。
-F freq 指定采样频率,单位为每秒多少次。
-g 收集调用栈信息。
-o file 指定输出文件名,默认为perf.data
-p pid 记录指定进程ID的性能。
-u 只记录用户空间的事件。
-k 只记录内核空间的事件。

三、perf report: 分析数据,找出真凶

有了perf.data这个黑匣子,我们就可以使用perf report来分析数据,找出程序中的性能瓶颈。

基本用法:

perf report

运行后,会打开一个交互式的界面,显示各个函数的性能统计信息。你可以使用上下方向键选择函数,回车键进入函数详情,查看更详细的统计信息。

进阶用法:

  1. 指定perf.data文件:

    如果perf.data文件不在当前目录下,可以使用-i选项指定:

    perf report -i /path/to/perf.data
  2. 生成文本报告:

    -n选项可以生成文本报告,方便离线分析:

    perf report -n > report.txt
  3. 查看调用图:

    如果使用perf record -g收集了调用栈信息,可以使用perf report -g查看调用图:

    perf report -g

    这会显示函数之间的调用关系,以及每个函数的性能占比,帮助你快速定位性能瓶颈。

  4. 注解源代码:

    perf annotate命令可以将性能数据和源代码关联起来,告诉你哪行代码消耗了最多的CPU时间:

    perf annotate

    这需要程序包含调试信息(编译时不要strip)。

分析perf report输出:

perf report的输出通常包含以下信息:

  • Overhead: 该函数占总CPU时间的百分比。
  • Command: 命令名称。
  • Shared Object: 共享对象(通常是可执行文件或动态链接库)。
  • Symbol: 函数名称。

关注Overhead最高的函数,它们很可能是性能瓶颈所在。

分析示例:

在执行了perf record -g ./example之后,运行perf report。你可能会看到类似这样的输出:

# Samples: 1K of event 'cycles'
# Event count (approx.): 119236788
#
# Overhead   Command  Shared Object       Symbol
# ........  ........  .................  ....................................
#   45.00%  example  example              slow_function(std::vector<int, std::allocator<int> > const&)
#   20.00%  example  libc-2.31.so         __GI___printf
#   15.00%  example  example              main
#   10.00%  example  [kernel.kallsyms]   __do_sys_read
#    5.00%  example  [kernel.kallsyms]   ...

可以看到,slow_function占用了45%的CPU时间,是性能瓶颈。 你可以使用perf annotate查看 slow_function 的源代码,找出性能瓶颈的具体位置,并进行优化。例如,可以尝试使用更高效的算法,或者减少循环次数。

表格总结perf report常用选项:

选项 描述
-i file 指定perf.data文件。
-n 生成文本报告。
-g 查看调用图。
-s symbol_name 过滤指定符号 (函数名)。
--stdio 将报告输出到标准输出。
-v 详细模式,输出更多信息。

四、实战演练:优化一个C++程序

假设我们有一个C++程序 bad_example.cpp,它的性能很差:

#include <iostream>
#include <vector>

int main() {
    std::vector<int> data(10000);
    for (int i = 0; i < data.size(); ++i) {
        data[i] = i;
    }

    long long sum = 0;
    for (int i = 0; i < data.size(); ++i) {
        for (int j = 0; j < data.size(); ++j) {
            sum += data[i] * data[j];
        }
    }

    std::cout << "Sum: " << sum << std::endl;

    return 0;
}
  1. 使用perf stat初步评估:

    g++ bad_example.cpp -o bad_example -O0
    perf stat ./bad_example

    观察cyclesinstructions等指标,确认程序性能较差。

  2. 使用perf record记录运行轨迹:

    perf record -g ./bad_example
  3. 使用perf report分析数据:

    perf report

    你会发现main函数占用了大部分CPU时间。

  4. 使用perf annotate注解源代码:

    perf annotate

    你会发现嵌套循环是性能瓶颈。

  5. 优化代码:

    将嵌套循环改为单循环,减少计算量:

    #include <iostream>
    #include <vector>
    
    int main() {
        std::vector<int> data(10000);
        for (int i = 0; i < data.size(); ++i) {
            data[i] = i;
        }
    
        long long sum = 0;
        long long data_sum = 0;
        for(int i = 0 ; i < data.size() ; ++i) {
            data_sum += data[i];
        }
        sum = data_sum * data_sum;
        std::cout << "Sum: " << sum << std::endl;
    
        return 0;
    }
  6. 重新编译并使用perf stat验证优化效果:

    g++ optimized_example.cpp -o optimized_example -O3
    perf stat ./optimized_example

    你会发现cyclesinstructions等指标都显著降低,程序性能得到了大幅提升。

总结:

perf工具链是C++程序员必备的性能分析利器。perf stat可以让你快速了解程序的整体性能状况,perf record可以记录程序运行时的各种事件,perf report可以分析数据,找出性能瓶颈。熟练掌握这三个工具,你就可以像医生一样,诊断程序的病情,开出药方,让程序跑得更快、更健康。记住,实践出真知!多用perf分析你的程序,你会发现很多意想不到的惊喜。

祝大家编码愉快,bug永不相见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注