在WSL上对C++程序进行性能分析.md

前言

当程序性能达到瓶颈时,我们可以通过多种工具对程序的性能进行分析,定位到最大的CPU耗时、IO耗时,从而对程序进行更细粒度的优化。

之前只用过 Jprofiler 对 java 进行分析,没有分析过 c++ 。由于我的 C++ 开发平台是 WSL ,与原生的 linux 内核有区别,需要额外的配置才能使用分析工具,因此写了一篇笔记来避免后续再踩坑。

C++ 的性能分析工具有很多,如:

gprof:这是一个GNU的性能分析工具,主要用于分析程序的函数调用关系,以及每个函数的运行时间等。

Valgrind:这是一个用于内存调试、内存泄漏检测以及性能分析的开源工具集。其中,Valgrind的Callgrind工具可以收集程序运行时的函数调用信息,用于性能分析。

perf:这是Linux下的一个性能分析工具,可以收集CPU使用情况、缓存命中率、分支预测错误等多种性能数据。

不同的分析工具针对的重点不同,我们可能需要同时使用多种工具进行分析。

引用:

gprof

gprof 通常作为 GNU Binutils 的一部分,在大多数 Linux 发行版中都是默认安装的。

gprof 是一个GNU项目中的性能分析工具,用于分析C和C++程序的函数调用图和每个函数的CPU使用时间。它通过测量程序执行过程中的函数调用频率和运行时间来帮助你识别出那些占用了最多运行时间的函数,从而定位可能的性能瓶颈。

gprof 能提供的分析项如下:

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
  %         the percentage of the total running time of the
time program used by this function.

cumulative a running sum of the number of seconds accounted
seconds for by this function and those listed above it.

self the number of seconds accounted for by this
seconds function alone. This is the major sort for this
listing.

calls the number of times this function was invoked, if
this function is profiled, else blank.

self the average number of milliseconds spent in this
ms/call function per call, if this function is profiled,
else blank.

total the average number of milliseconds spent in this
ms/call function and its descendents per call, if this
function is profiled, else blank.

name the name of the function. This is the minor sort
for this listing. The index shows the location of
the function in the gprof listing. If the index is
in parenthesis it shows where it would appear in
the gprof listing if it were to be printed.

可以看到, gprof 主要提供了 调用次数、耗时 的两项分析。

总的来说,如果你的编译器是 gcc ,那么你可以直接使用 gprof 来分析程序性能。 gprof 是一个使用非常简单的分析工具,但它的功能相对较少, gprof 适合初步的程序分析。

gprof 使用方式

我的系统是 win10 下的 wsl 。但 wsl1 不支持 gprof ,在 wsl1 上使用 gprof 可能会得到全0的耗时数据,原因是 wsl1 不支持 setitimer(ITIMER_PROF, …) 函数。windows 19013 及更新的版本中的 WSL 2 支持了 gprof ,如果你使用的是 wsl1 ,必须升级到 wsl2 才能使用 gprof 。

具体的升级步骤可以阅读 wsl1 升级到 wsl2 小节。

引用:

(1) 直接编译

当你的程序比较小,可以直接使用指令编译时,可以通过添加编译指令的方式包含 gprof 。

整体流程是: 编译 -> 运行程序 -> 使用 gprof 工具来分析 gmon.out 文件中的数据 -> 分析结果

1、编译

在编译你的程序时,你需要使用 -pg 选项来告诉编译器包含 gprof 的分析代码。例如:

1
g++ -pg -o myprogram hello.cpp

这将生成一个可执行文件 myprogram,其中包含了 gprof 所需的性能分析代码。

2、运行程序

运行你的程序,就像平常一样。gprof 会收集运行时信息,并将这些信息写入一个名为 gmon.out 的文件中。

1
./myprogram

3、使用gprof分析数据

现在,你可以使用 gprof 工具来分析 gmon.out 文件中的数据。执行以下命令:

1
gprof myprogram gmon.out > analysis.txt

这将会生成一个名为 analysis.txt 的文本文件,其中包含了 gprof 的分析结果。这个文件包含了函数调用图、每个函数的调用次数、每个函数消耗的CPU时间等信息。

4、阅读分析结果

打开 analysis.txt 文件,你可以看到类似下面的输出:

1
2
3
4
5
6
7
8
Flat profile:  

Each sample counts as 0.01 seconds.
% cumulative self self total
time seconds seconds calls Ts/call Ts/call name
...
12.34 0.12 0.12 123 0.00 0.12 function_name
...

摘自

(2) Cmake 编译

如果你的项目是一个使用 Cmake 进行管理的项目,可以直接使用 Cmake 编译指令进行编译。

1、编译

与往常一样,在编译指令中添加 -DCMAKE_CXX_FLAGS=-pg ,即可附加编译 gprof ,编译流程如下:

1
2
3
4
5
cd build

cmake -DCMAKE_C_FLAGS=-pg -DCMAKE_CXX_FLAGS=-pg -DCMAKE_BUILD_TYPE=Debug ..

make

如果你的 CmakeList 中已有编译指令,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
if (CMAKE_CXX_COMPILER_ID MATCHES "Clang")
SET( CMAKE_CXX_FLAGS "-Ofast -std=c++11 -DHAVE_CXX0X -openmp -fpic -ftree-vectorize" )
check_cxx_compiler_flag("-march=native" COMPILER_SUPPORT_NATIVE_FLAG)
if(COMPILER_SUPPORT_NATIVE_FLAG)
SET( CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -march=native" )
message("set -march=native flag")
else()
check_cxx_compiler_flag("-mcpu=apple-m1" COMPILER_SUPPORT_M1_FLAG)
if(COMPILER_SUPPORT_M1_FLAG)
SET( CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mcpu=apple-m1" )
message("set -mcpu=apple-m1 flag")
endif()
endif()
elseif (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
if (CMAKE_BUILD_TYPE STREQUAL "Debug")
SET( CMAKE_CXX_FLAGS "-O0 -lrt -std=c++11 -DHAVE_CXX0X -march=native -fpic -w -fopenmp" )
else ()
SET( CMAKE_CXX_FLAGS "-Ofast -lrt -std=c++11 -DHAVE_CXX0X -march=native -fpic -w -fopenmp -ftree-vectorize -ftree-vectorizer-verbose=0" )
endif()
elseif (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
SET( CMAKE_CXX_FLAGS "/O2 -DHAVE_CXX0X /W1 /openmp /EHsc" )
endif()

可能需要直接修改编译指令,我的修改如下:

1
2
if (CMAKE_BUILD_TYPE STREQUAL "Debug")
SET( CMAKE_CXX_FLAGS "-O0 -lrt -std=c++11 -DHAVE_CXX0X -march=native -fpic -w -fopenmp -g" )

其余步骤与 直接编译 中一致。

摘自

wsl1 升级到 wsl2

可以阅读这篇博客,记录的非常细致,亲测没什么问题:

https://blog.heyfe.org/blog/wsl-upgrade.html

更新结束后如果打不开 wsl ,可以尝试重启。

perf 与 火焰图

perf (performance 的缩写)是 Linux 系统原生提供的性能分析工具,会返回 CPU 正在执行的函数名以及调用栈(stack)。它的主要作用是对程序的调用栈进行采样分析,通过调用栈反推出函数的调用次数、关系和CPU消耗时间。

使用流程

(1)安装 perf

与 gprof 一样,WSL1 同样不支持 perf 。因此需要升级至 WSL2 。此外,WSL 官方应用源中没有提供 perf 工具,还需额外自行编译对应 WSL 版本内核的官方 Linux 内核中的 perf 并安装。

1、获取 Linux 内核版本号 并 获取 Linux 内核源码

这一步主要是获取 Linux 的版本号,获取对应版本的 kernal 源码,解压并进入目录。但是首先记得安装编译工具:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ sudo apt install build-essential flex bison dwarves libssl-dev libelf-dev cpio

$ uname -r
5.10.16.3-microsoft-standard-WSL2

$ wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.10.16.tar.xz

--2024-11-22 13:33:36-- https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.10.16.tar.xz
Resolving cdn.kernel.org (cdn.kernel.org)... 198.18.0.12, 2a04:4e42:1a::432
Connecting to cdn.kernel.org (cdn.kernel.org)|198.18.0.12|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 116264828 (111M) [application/x-xz]
Saving to: ‘linux-5.10.16.tar.xz.1’

linux-5.10.16.tar.xz.1 100%[=================================================>] 110.88M 922KB/s in 2m 2s

2024-11-22 13:35:42 (932 KB/s) - ‘linux-5.10.16.tar.xz.1’ saved [116264828/116264828]

$ tar -xf linux-5.10.16.tar.xz
$ cd linux-5.10.16

2、编译 perf 并安装

进入内核源码目录的 tools/perf 子目录并进行编译:

1
2
$ cd tools/perf
$ make -j $(nproc) KCONFIG_CONFIG=../../Microsoft/config-wsl

编译过程会生成一个名为 perf 的二进制文件。将编译好的 perf 工具安装到系统路径中:

1
$ sudo cp perf /usr/local/bin/

3、测试

通过以下命令检查perf是否正常工作:

1
2
$ perf --version
perf version 5.10.16

如果你看到perf的版本信息,那么说明安装成功。

但是,事实上, perf 的功能不一定安装完全,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Auto-detecting system features:
... dwarf: [ OFF ]
... dwarf_getlocations: [ OFF ]
... glibc: [ on ]
... libbfd: [ OFF ]
... libbfd-buildid: [ OFF ]
... libcap: [ OFF ]
... libelf: [ on ]
... libnuma: [ OFF ]
... numa_num_possible_cpus: [ OFF ]
... libperl: [ OFF ]
... libpython: [ on ]
... libcrypto: [ on ]
... libunwind: [ OFF ]
... libdw-dwarf-unwind: [ OFF ]
... zlib: [ on ]
... lzma: [ OFF ]
... get_cpuid: [ on ]
... bpf: [ on ]
... libaio: [ on ]
... libzstd: [ OFF ]
... disassembler-four-args: [ OFF ]

有很多功能被 off 掉了,这是因为 perf 编译的时候,会根据你本地安装的库的版本来决定是否支持某些功能,如果你需要全功能的 perf 则需要进一步安装依赖库:

1
2
3
$ sudo apt install binutils-dev debuginfod default-jdk default-jre libaio-dev libbabeltrace-dev libcap-dev libdw-dev libdwarf-dev libelf-dev libiberty-dev liblzma-dev libnuma-dev libperl-dev libpfm4-dev libslang2-dev libssl-dev libtraceevent-dev libunwind-dev libzstd-dev libzstd1 python-setuptools python3 python3-dev systemtap-sdt-dev zlib1g-dev

$ make clean && make

如果只需要生成火焰图来分析,也可以不安装上述的依赖库。

引用:

(2)安装 FlameGraph 火焰图分析工具

perf 生成的调用链不太方便查看,因此需要火焰图生成工具把数据可视化,便于分析。这里需要使用开源的 FlameGraph 来根据统计信息生成火焰图。

1、下载火焰图生成工具 FlameGraph

1
git clone https://github.com/brendangregg/FlameGraph.git

(3)运行程序、监控、生成并查看火焰图

与 gprof 不同, perf 可以直接 att到正在运行的程序并进行分析,因此不需要额外的编译配置。

在按照上述的方案安装 perf 后,我们可以执行程序并记录录制CPU执行堆栈:

1
sudo perf record -F 99 -a -g -p `pidof ./bin/test` -o perf.data -- sleep 30

上面的代码中,perf record 表示记录,-F 99 表示每秒99次,-a 表示对所有的 CPU 核采样,-g 表示记录调用栈,-p 指定要分析的进程号,-o 指定输出文件(默认为perf.data),sleep 30 则是持续30秒。

运行后会在当前目录产生一个默认名为 perf.data 的文本文件。如果一台服务器有 16 个 CPU ,每秒抽样99次,持续30秒,就得到 47,520 个调用栈,长达几十万甚至上百万行,文件会非常庞大。

为了便于阅读,perf record 命令可以统计每个调用栈出现的百分比,然后从高到低排列。

1
sudo perf report -i perf.data -n --stdio -f

但是,这个分析结果还是不易读,因此需要根据数据生成火焰图,来进一步分析。 FlameGraph 是一个一键将统计数据生成火焰图的开源工具,这是项目主页:

https://github.com/brendangregg/FlameGraph

工具使用主要有两步:将统计数据压缩为单行,将单行数据转为火焰图。

FlameGraph 提供了多种压缩工具,列表如下。可以去 github 阅读详细的描述:

1
2
3
4
5
6
7
8
9
10
11
12
stackcollapse.pl: for DTrace stacks
stackcollapse-perf.pl: for Linux perf_events "perf script" output
stackcollapse-pmc.pl: for FreeBSD pmcstat -G stacks
stackcollapse-stap.pl: for SystemTap stacks
stackcollapse-instruments.pl: for XCode Instruments
stackcollapse-vtune.pl: for Intel VTune profiles
stackcollapse-ljp.awk: for Lightweight Java Profiler
stackcollapse-jstack.pl: for Java jstack(1) output
stackcollapse-gdb.pl: for gdb(1) stacks
stackcollapse-go.pl: for Golang pprof stacks
stackcollapse-vsprof.pl: for Microsoft Visual Studio profiles
stackcollapse-wcp.pl: for wallClockProfiler output

使用这些工具对不同的分析器的分析数据进行压缩,可以得到统一的分析数据:

1
2
3
4
5
For perf_events:
$ ./stackcollapse-perf.pl out.perf > out.folded

For DTrace:
$ ./stackcollapse.pl out.kern_stacks > out.kern_folded

在压缩后,可以使用生成工具 flamegraph.pl ,分析压缩文件来生成 SVG 格式的火焰图:

1
$ ./flamegraph.pl out.kern_folded > kernel.svg
1
2
3
4
5
6
# 根据perf.data生成火焰图的步骤
sudo perf script -i perf.data > perf.unfold
./FlameGraph/stackcollapse-perf.pl perf.unfold > perf.folded
./FlameGraph/flamegraph.pl perf.folded > perf.svg
# 以上步骤也可以合为下边的指令
sudo perf script -i perf.data | ./FlameGraph/stackcollapse-perf.pl | ./FlameGraph/flamegraph.pl > perf.svg

引用:

Valgrind

挖个坑。下周来写