哈喽,各位好!今天我们要聊聊C++程序员的秘密武器——perf
工具链,特别是perf stat
、perf record
和perf 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
: 分支指令数。程序中if
、else
、switch
等分支语句的数量。branch-misses
: 分支预测失败次数。CPU预测错误分支的次数。分支预测失败会导致流水线阻塞,降低性能。
进阶用法:
-
指定事件:
你可以指定
perf stat
监测的事件。例如,只想看缓存相关的统计:perf stat -e cache-references,cache-misses ./your_program
perf list
可以查看所有支持的事件。事件种类繁多,包括硬件事件(如CPU周期、缓存命中)和软件事件(如上下文切换、页面错误)。 -
指定时间间隔:
-I
选项可以指定统计的时间间隔(毫秒):perf stat -I 1000 ./your_program
这会每隔1秒输出一次统计结果,方便你观察程序运行过程中性能的变化。
-
统计子进程:
-c
选项可以统计子进程的性能:perf stat -c ./your_program
如果你的程序会创建子进程,这个选项很有用。
-
前后台运行,指定文件输出:
将
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
分析。对比两次运行的输出,你会发现开启优化后,cycles
、instructions
等指标都显著降低,IPC
显著升高,程序性能得到了提升。
表格总结perf stat
常用选项:
选项 | 描述 |
---|---|
-e event |
指定要监测的事件,如cycles 、cache-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
文件。
进阶用法:
-
指定事件:
和
perf stat
一样,你可以指定perf record
记录的事件:perf record -e cycles,cache-misses ./your_program
-
指定采样频率:
-F
选项可以指定采样频率(每秒多少次)。采样频率越高,记录的数据越详细,但也会带来更大的开销:perf record -F 99 ./your_program
-F 99
表示每秒采样99次。 -
收集调用栈:
-g
选项可以收集调用栈信息,这对于分析性能瓶颈非常有帮助。perf record -g ./your_program
注意:使用
-g
选项需要程序包含调试信息(编译时不要strip)。 -
只记录用户空间的事件:
-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 |
指定要记录的事件,如cycles 、cache-misses 等。 |
-F freq |
指定采样频率,单位为每秒多少次。 |
-g |
收集调用栈信息。 |
-o file |
指定输出文件名,默认为perf.data 。 |
-p pid |
记录指定进程ID的性能。 |
-u |
只记录用户空间的事件。 |
-k |
只记录内核空间的事件。 |
三、perf report
: 分析数据,找出真凶
有了perf.data
这个黑匣子,我们就可以使用perf report
来分析数据,找出程序中的性能瓶颈。
基本用法:
perf report
运行后,会打开一个交互式的界面,显示各个函数的性能统计信息。你可以使用上下方向键选择函数,回车键进入函数详情,查看更详细的统计信息。
进阶用法:
-
指定
perf.data
文件:如果
perf.data
文件不在当前目录下,可以使用-i
选项指定:perf report -i /path/to/perf.data
-
生成文本报告:
-n
选项可以生成文本报告,方便离线分析:perf report -n > report.txt
-
查看调用图:
如果使用
perf record -g
收集了调用栈信息,可以使用perf report -g
查看调用图:perf report -g
这会显示函数之间的调用关系,以及每个函数的性能占比,帮助你快速定位性能瓶颈。
-
注解源代码:
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;
}
-
使用
perf stat
初步评估:g++ bad_example.cpp -o bad_example -O0 perf stat ./bad_example
观察
cycles
、instructions
等指标,确认程序性能较差。 -
使用
perf record
记录运行轨迹:perf record -g ./bad_example
-
使用
perf report
分析数据:perf report
你会发现
main
函数占用了大部分CPU时间。 -
使用
perf annotate
注解源代码:perf annotate
你会发现嵌套循环是性能瓶颈。
-
优化代码:
将嵌套循环改为单循环,减少计算量:
#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; }
-
重新编译并使用
perf stat
验证优化效果:g++ optimized_example.cpp -o optimized_example -O3 perf stat ./optimized_example
你会发现
cycles
、instructions
等指标都显著降低,程序性能得到了大幅提升。
总结:
perf
工具链是C++程序员必备的性能分析利器。perf stat
可以让你快速了解程序的整体性能状况,perf record
可以记录程序运行时的各种事件,perf report
可以分析数据,找出性能瓶颈。熟练掌握这三个工具,你就可以像医生一样,诊断程序的病情,开出药方,让程序跑得更快、更健康。记住,实践出真知!多用perf
分析你的程序,你会发现很多意想不到的惊喜。
祝大家编码愉快,bug永不相见!