在Linux上对C++代码进行性能分析的方法

在Linux上对C++代码进行性能分析的方法

技术背景

在Linux系统上开发C++程序时,性能优化是一个重要的环节。为了找出代码中的性能瓶颈,我们需要使用性能分析工具来收集和分析程序的运行信息。不同的性能分析工具具有不同的特点和适用场景,下面将介绍几种常见的C++性能分析方法和工具。

实现步骤

手动中断法

在调试器(如gdb)中运行代码,当程序运行缓慢时手动中断程序,多次查看调用栈(如backtrace)。如果某段代码占用了一定比例的时间,那么在每次采样中就有相应的概率捕获到它。多次清理性能问题后,剩余的问题会更容易被发现,这种“放大效应”可能会带来显著的性能提升。

使用Valgrind和Callgrind

  1. 安装Valgrind和KCacheGrind:
1
sudo apt install kcachegrind valgrind
  1. 编译并运行程序:
1
2
gcc -ggdb3 -O3 -std=c99 -Wall -Wextra -pedantic -o main.out main.c
time valgrind --tool=callgrind --dump-instr=yes --collect-jumps=yes ./main.out 10000
  1. 查看分析结果:
1
kcachegrind callgrind.out.<pid>

使用gprof

  1. 编译时添加-pg选项:
1
gcc -pg -ggdb3 -O3 -std=c99 -Wall -Wextra -pedantic -o main.out main.c
  1. 运行程序:
1
time ./main.out 10000
  1. 生成分析报告:
1
gprof main.out > main.gprof
  1. 生成可视化图表(可选):
1
2
3
sudo apt install graphviz
python3 -m pip install --user gprof2dot
gprof2dot < main.gprof | dot -Tsvg -o output.svg

使用perf工具

  1. 安装perf工具:
1
sudo apt install linux-tools-common linux-tools-generic
  1. 配置内核参数:
1
sudo sysctl kernel.perf_event_paranoid=-1 kernel.kptr_restrict=0
  1. 收集性能数据:
1
time perf record --call-graph dwarf ./main.out 10000
  1. 交互式查看数据:
1
perf report

使用gperftools

  1. 安装gperftools:
1
sudo apt install google-perftools
  1. 在运行时启用CPU分析器:
1
2
gcc -ggdb3 -O3 -std=c99 -Wall -Wextra -pedantic -o main.out main.c
LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libprofiler.so CPUPROFILE=prof.out ./main.out 10000
  1. 查看分析结果:
1
2
google-pprof --callgrind main.out prof.out > callgrind.out
kcachegrind callgrind.out

核心代码

以下是一个简单的C++测试程序示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>

uint64_t __attribute__ ((noinline)) common(uint64_t n, uint64_t seed) {
for (uint64_t i = 0; i < n; ++i) {
seed = (seed * seed) - (3 * seed) + 1;
}
return seed;
}

uint64_t __attribute__ ((noinline)) fast(uint64_t n, uint64_t seed) {
uint64_t max = (n / 10) + 1;
for (uint64_t i = 0; i < max; ++i) {
seed = common(n, (seed * seed) - (3 * seed) + 1);
}
return seed;
}

uint64_t __attribute__ ((noinline)) maybe_slow(uint64_t n, uint64_t seed, int is_slow) {
uint64_t max = n;
if (is_slow) {
max *= 10;
}
for (uint64_t i = 0; i < max; ++i) {
seed = common(n, (seed * seed) - (3 * seed) + 1);
}
return seed;
}

int main(int argc, char **argv) {
uint64_t n, seed;
if (argc > 1) {
n = strtoll(argv[1], NULL, 0);
} else {
n = 1;
}
if (argc > 2) {
seed = strtoll(argv[2], NULL, 0);
} else {
seed = 0;
}
seed += maybe_slow(n, seed, 0);
seed += fast(n, seed);
seed += maybe_slow(n, seed, 1);
seed += fast(n, seed);
seed += maybe_slow(n, seed, 0);
seed += fast(n, seed);
printf("%" PRIX64 "\n", seed);
return EXIT_SUCCESS;
}

最佳实践

  • 对于CPU密集型应用程序,先在DEBUG模式下使用性能分析工具找出可疑代码段,然后在RELEASE模式下注释掉这些代码段,观察性能变化。
  • 对于I/O密集型应用程序,在RELEASE模式下使用性能分析工具找出可疑代码段。
  • 结合多种性能分析工具进行分析,以获取更全面的信息。

常见问题

gprof相关问题

  • 多线程代码:gprof在多线程代码中可能无法正常工作,需要使用workaround
  • 函数指针:gprof的调用图在处理函数指针时可能会混乱。
  • 调用计数不准确:gprof的调用计数可能与实际情况存在较大差异。

Valgrind相关问题

  • 性能开销大:Valgrind会导致程序运行速度大幅下降,对于大型工作负载可能会成为严重限制。

perf相关问题

  • 数据丢失:与gprof的插桩代码相比,perf会丢失一些数据,例如不知道每个函数的调用次数。
  • 存在[unknown]函数:在分析结果中可能会出现[unknown]函数,具体原因需要进一步排查。

kcachegrind相关问题

  • 无法显示完整调用图:在复杂的C++软件中,kcachegrind可能无法显示完整的调用图。
  • <cycle N>条目含义不明:在复杂的C++软件中,可能会看到<cycle N>类型的条目,不清楚其具体含义。

在Linux上对C++代码进行性能分析的方法
https://119291.xyz/posts/c-plus-plus-code-profiling-on-linux/
作者
ww
发布于
2025年5月21日
许可协议