【C/C++ 集成内存调试、内存泄漏检测和性能分析的工具 Valgrind 】Linux 下 Valgrind 工具的全面使用指南


在这里插入图片描述
在这里插入图片描述

读音为 [wɑːɡrɪnd]。

Valgrind 工具的安装

Valgrind 是一个用于内存调试、内存泄漏检测和性能分析的开源工具。以下是在 Ubuntu 或其他 Debian 系统上安装 Valgrind 的步骤:

  1. 打开终端。
  2. 首先,你需要更新你的系统包列表。可以使用以下命令:
    sudo apt-get update
    
  3. 然后,使用以下命令安装 Valgrind:
    sudo apt-get install valgrind
    
  4. 安装完成后,你可以使用以下命令验证 Valgrind 是否已成功安装:
    valgrind --version
    
    如果 Valgrind 已成功安装,这将显示 Valgrind 的版本号。

对于其他 Linux 发行版,如 Fedora 或 CentOS,你可以使用相应的包管理器(如 yum 或 dnf)来安装 Valgrind。例如,在 Fedora 上,你可以使用以下命令安装 Valgrind:

sudo dnf install valgrind

在 macOS 上,你可以使用 Homebrew 来安装 Valgrind:

brew install valgrind

请注意,Valgrind 可能不支持最新版本的 macOS。

在 Windows 上,Valgrind 不直接可用,但你可以通过 Windows 的子系统 Linux(WSL)来使用它。

交叉编译Valgrind

Valgrind 的源代码包含了它运行所需的所有库,因此在大多数情况下,你不需要额外的依赖库就可以编译和运行 Valgrind。然而,编译 Valgrind 需要一些基本的开发工具,包括一个 C 编译器(如 gcc)和 make 工具。

如果你打算在一个不同的平台上交叉编译 Valgrind,你需要一个为那个平台配置的交叉编译器。你还需要确保你的编译环境包含了所有 Valgrind 需要的头文件和库。

以下是交叉编译 Valgrind 的基本步骤:

  1. 首先,下载 Valgrind 的源代码。你可以从 Valgrind 的官方网站下载最新的源代码:http://valgrind.org/downloads/current.html
  2. 解压源代码包,并进入源代码目录:
    tar xvf valgrind-3.17.0.tar.bz2
    cd valgrind-3.17.0
    
    (请根据你下载的 Valgrind 版本调整上述命令中的版本号)
  3. 配置编译环境。你需要指定你的交叉编译器和目标平台。例如,如果你的交叉编译器是 arm-linux-gnueabi-gcc,并且你的目标平台是 arm-linux,你可以使用以下命令:
    ./configure --host=arm-linux CC=arm-linux-gnueabi-gcc
    
  4. 编译 Valgrind:
    make
    
  5. 最后,你可以将编译好的 Valgrind 复制到你的目标系统,或者使用 make install 命令将其安装到你的交叉编译环境中。

请注意,这只是一个基本的示例,你可能需要根据你的具体需求和你的交叉编译环境调整这些步骤。

Valgrind 工具的作用

Valgrind 是一个非常强大的工具,主要用于内存管理错误检测,以及 CPU 和内存分析。以下是一些基本的使用方法:

  1. 内存泄漏检测

    这是 Valgrind 最常用的功能之一。你可以使用以下命令来检查你的程序是否有内存泄漏:

    valgrind --leak-check=yes your_program [your_program_arguments]
    

    这将运行你的程序,并在程序结束后报告任何内存泄漏。--leak-check=yes 选项告诉 Valgrind 检查内存泄漏。

  2. 使用 Massif 进行堆栈分析

    Massif 是 Valgrind 的一个工具,用于分析你的程序使用了多少堆栈。你可以使用以下命令来运行 Massif:

    valgrind --tool=massif your_program [your_program_arguments]
    

    这将生成一个名为 massif.out.pid 的文件,其中 pid 是你的程序的进程 ID。你可以使用 ms_print 命令来查看这个文件的内容:

    ms_print massif.out.pid
    
  3. 使用 Callgrind 进行性能分析

    Callgrind 是 Valgrind 的一个工具,用于分析你的程序的性能。你可以使用以下命令来运行 Callgrind:

    valgrind --tool=callgrind your_program [your_program_arguments]
    

    这将生成一个名为 callgrind.out.pid 的文件,其中 pid 是你的程序的进程 ID。你可以使用 callgrind_annotate 命令来查看这个文件的内容:

    callgrind_annotate callgrind.out.pid
    

以上只是 Valgrind 的一些基本用法。Valgrind 还有许多其他的功能和选项,你可以查阅 Valgrind 的官方文档来了解更多:http://valgrind.org/docs/manual/index.html

Memcheck 内存泄漏检测工具

请注意,需要确保你的程序和动态库都是使用调试信息(例如,使用 gcc 的 -g 选项)编译的,这样 Valgrind 才能提供更详细的报告。

常规检测(程序结束后生成报告)

Valgrind 的内存泄漏检测工具 Memcheck 可以精确到源代码的行号,告诉你哪一行代码分配的内存没有被正确释放。但是,为了能够做到这一点,你需要在编译你的程序时包含调试信息。

如果你使用 GCC 或 Clang 编译你的程序,你可以添加 -g 选项来包含调试信息:

gcc -g -o your_program your_program.c

然后,你可以使用 Valgrind 运行你的程序:

valgrind --tool=memcheck  --leak-check=full ./your_program

在报告的结果中,Valgrind 将显示导致内存泄漏的代码行。例如:

==12345== 40 bytes in 1 blocks are definitely lost in loss record 1 of 2
==12345==    at 0x4C2FB0F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==12345==    by 0x108671: main (your_program.c:2)

在这个例子中,your_program.c:2 表示内存泄漏发生在 your_program.c 文件的第 2 行。

请注意,如果你的程序是用优化选项(如 -O2-O3)编译的,某些优化可能会使得行号信息变得不准确。如果可能,你应该在没有优化的情况下编译你的程序进行内存泄漏检测。

重要参数


  • --leak-check=yes 会告诉 Valgrind 进行内存泄漏检测,但它只会提供每个泄漏点的总体信息,例如泄漏的总字节数和泄漏的块数。
  • --leak-check=full 则会提供更详细的信息。除了泄漏点的总体信息,它还会显示每个单独的泄漏块的信息,包括它的大小和分配它的函数的堆栈跟踪。这可以帮助你更准确地定位内存泄漏的位置。
  • --show-leak-kinds=all 显示所有类型的内存泄漏,包括 “definitely lost”、“indirectly lost”、“possibly lost” 和 “still reachable”。
  • --num-callers=n 增加调用堆栈的深度。

如果你使用 --leak-check=full,你会得到更详细的输出:

==12345== 128 bytes in 1 blocks are definitely lost in loss record 1 of 1
==12345==    at 0x4C2FB0F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==12345==    by 0x108671: func (your_program.c:4)
==12345==    by 0x108687: main (your_program.c:8)

这显示了内存泄漏是在 func 函数中的 your_program.c:4 行发生的,这可以帮助你更准确地找到问题的位置。

长时间运行的服务

Valgrind 默认在程序结束时报告内存泄漏和其他问题。然而,如果你的程序是一个长时间运行的服务或者你希望在运行过程中查看报告,你可以使用 Valgrind 的 gdbserver 模式,这允许你在运行时与 Valgrind 交互。

以下是使用 gdbserver 模式的基本步骤:

  1. 首先,启动你的程序,使用 --vgdb=yes 选项让 Valgrind 在启动时启动 gdbserver:
    valgrind --vgdb=yes --leak-check=full your_program [your_program_arguments]
    
  2. 在另一个终端中,你可以使用 gdb 连接到 Valgrind。首先,你需要找到你的程序的进程 ID(PID)。然后,使用以下命令连接到 Valgrind:
    gdb your_program
    (gdb) target remote | vgdb
    
    这将启动 gdb 并连接到 Valgrind。
  3. 现在,你可以在 gdb 中使用 monitor 命令来与 Valgrind 交互。例如,你可以使用 monitor leak_check full reachable any 命令来在运行时检查内存泄漏:
    (gdb) monitor leak_check full reachable any
    
    这将告诉 Valgrind 立即执行一个完全的内存泄漏检查,并报告所有可达和不可达的内存泄漏。

请注意,这只是一个基本的示例,你可能需要根据你的具体需求调整这些步骤。你可以查阅 Valgrind 的官方文档来了解更多关于 gdbserver 模式的信息:http://valgrind.org/docs/manual/manual-core-adv.html#manual-core-adv.gdbserver

使报告输出至文件

默认情况下内存泄漏包含会直接跟随进程输出到控制台,但是如果开了--show-leak-kinds=all 参数,报告会达到上万行,这时候就需要使其输出至文件

你可以将Valgrind的输出重定向到一个文件。你可以在命令行中使用>操作符来实现这个功能。以下是一个例子:

valgrind --leak-check=full --show-leak-kinds=all your_program > output.txt

这条命令会将Valgrind的输出保存到output.txt文件中。你可以将output.txt替换为你想要的任何文件名。

此外,Valgrind还提供了一个--log-file选项,你可以使用这个选项来指定输出文件。以下是一个例子:

valgrind --leak-check=full --show-leak-kinds=all --log-file=output.txt your_program

这条命令和上面的命令效果相同,都会将Valgrind的输出保存到output.txt文件中。

报告分析

示例一

==4197== LEAK SUMMARY:
==4197==    definitely lost: 6,624 bytes in 2 blocks
==4197==    indirectly lost: 0 bytes in 0 blocks
==4197==      possibly lost: 12,864 bytes in 34 blocks
==4197==    still reachable: 404,895,424 bytes in 504,849 blocks
==4197==                       of which reachable via heuristic:
==4197==                         multipleinheritance: 240 bytes in 1 blocks
==4197==         suppressed: 0 bytes in 0 blocks
==4197== Reachable blocks (those to which a pointer was found) are not shown.
==4197== To see them, rerun with: --leak-check=full --show-leak-kinds=all
==4197==
==4197== For lists of detected and suppressed errors, rerun with: -s
==4197== ERROR SUMMARY: 32 errors from 32 contexts (suppressed: 0 from 0)

分析_示例一

下面是对这个报告的解析:

  • definitely lost: 6,624 bytes in 2 blocks:这表示有6624字节的内存在2个块中被确定性地丢失了。这通常意味着这些内存已经被分配出去,但是没有被释放,而且程序也无法再次访问到这些内存。这是一个严重的内存泄露。
  • indirectly lost: 0 bytes in 0 blocks:这表示没有间接丢失的内存。间接丢失的内存是指,由于直接丢失的内存块包含了指向这些内存的指针,因此这些内存也被视为丢失。
  • possibly lost: 12,864 bytes in 34 blocks:这表示可能丢失了12864字节的内存在34个块中。可能丢失的内存是指,这些内存可能还在被使用,也可能已经丢失。
  • still reachable: 404,895,424 bytes in 504,849 blocks:这表示还有404895424字节的内存在504849个块中仍然可以被访问。这些内存在程序结束时没有被释放,但是如果需要的话,程序还是可以访问到这些内存的。这不一定是一个问题,但是如果这个数字持续增长,那么可能会导致内存泄露。
  • suppressed: 0 bytes in 0 blocks:这表示没有被抑制的错误。抑制的错误是指那些被用户明确告知Valgrind忽略的错误。
  • ERROR SUMMARY: 32 errors from 32 contexts:这表示Valgrind在32个不同的上下文中找到了32个错误。

这个报告表明,你的程序可能存在内存泄露问题。你应该检查你的代码,特别是那些分配和释放内存的部分,以确保所有分配的内存在使用完毕后都被正确地释放。

在查看报告时,你应该关注以下几个关键字:

  • “definitely lost”:这表示有内存被分配了,但是没有被释放,而且程序也无法再次访问到这些内存。这是一个严重的内存泄露。
  • “possibly lost”:这表示有内存可能还在被使用,也可能已经丢失。这种情况通常发生在程序使用了复杂的数据结构,如循环链表等。
  • “still reachable”:这表示有内存在程序结束时没有被释放,但是如果需要的话,程序还是可以访问到这些内存的。这不一定是一个问题,但是如果这个数字持续增长,那么可能会导致内存泄露。
  • “indirectly lost”:这表示有内存是因为直接丢失的内存块包含了指向这些内存的指针,因此这些内存也被视为丢失。

示例二

==4761== 320 bytes in 1 blocks are possibly lost in loss record 9,331 of 11,803
==4761==    at 0x484DA83: calloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==4761==    by 0x40147D9: calloc (rtld-malloc.h:44)
==4761==    by 0x40147D9: allocate_dtv (dl-tls.c:375)
==4761==    by 0x40147D9: _dl_allocate_tls (dl-tls.c:634)
==4761==    by 0x4F08834: allocate_stack (allocatestack.c:430)
==4761==    by 0x4F08834: pthread_create@@GLIBC_2.34 (pthread_create.c:647)
==4761==    by 0xCEAF03A: zmq::thread_t::start(void (*)(void*), void*) (in /home/lzy/work/acvite_code/test/mfc5j3_appsw_libconvert/build/ubuntu/output/lib/libspi-protocol-convert.so)
==4761==    by 0xCE9FB3B: zmq::epoll_t::start() (in /home/lzy/work/acvite_code/test/mfc5j3_appsw_libconvert/build/ubuntu/output/lib/libspi-protocol-convert.so)
==4761==    by 0xCE8A35E: zmq::reaper_t::start() (in /home/lzy/work/acvite_code/test/mfc5j3_appsw_libconvert/build/ubuntu/output/lib/libspi-protocol-convert.so)
==4761==    by 0xCE7C833: zmq::ctx_t::create_socket(int) (in /home/lzy/work/acvite_code/test/mfc5j3_appsw_libconvert/build/ubuntu/output/lib/libspi-protocol-convert.so)
==4761==    by 0xCE7A2C5: zmq_socket (in /home/lzy/work/acvite_code/test/mfc5j3_appsw_libconvert/build/ubuntu/output/lib/libspi-protocol-convert.so)
==4761==    by 0xCE633EA: PubCANOutput::InitPub() (spi-service-protocol.h:33)
==4761==    by 0xCE63A93: PubCANOutput::IpcCANOutputPub(unsigned char const*, int) (spi-service-protocol.h:65)
==4761==    by 0xCE60F90: Send (spi-protocol-convert.h:22)
==4761==    by 0xCE60F90: Send (protocol-convert-base.h:49)
==4761==    by 0xCE60F90: HobotADAS::SPIProtocolConvert::OnMessageEnd(long const&, long const&) (spi-protocol-convert.cc:134)
==4761==    by 0x2AE98C: HobotADAS::CommPluginManager::ProcessMsg() (comm_plugin_manager.cpp:321)
==4761==
==4761== 320 bytes in 1 blocks are possibly lost in loss record 9,332 of 11,803
==4761==    at 0x484DA83: calloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==4761==    by 0x40147D9: calloc (rtld-malloc.h:44)
==4761==    by 0x40147D9: allocate_dtv (dl-tls.c:375)
==4761==    by 0x40147D9: _dl_allocate_tls (dl-tls.c:634)
==4761==    by 0x4F08834: allocate_stack (allocatestack.c:430)
==4761==    by 0x4F08834: pthread_create@@GLIBC_2.34 (pthread_create.c:647)
==4761==    by 0xCEAF03A: zmq::thread_t::start(void (*)(void*), void*) (in /home/lzy/work/acvite_code/test/mfc5j3_appsw_libconvert/build/ubuntu/output/lib/libspi-protocol-convert.so)
==4761==    by 0xCE9FB3B: zmq::epoll_t::start() (in /home/lzy/work/acvite_code/test/mfc5j3_appsw_libconvert/build/ubuntu/output/lib/libspi-protocol-convert.so)
==4761==    by 0xCE8281C: zmq::io_thread_t::start() (in /home/lzy/work/acvite_code/test/mfc5j3_appsw_libconvert/build/ubuntu/output/lib/libspi-protocol-convert.so)
==4761==    by 0xCE7C916: zmq::ctx_t::create_socket(int) (in /home/lzy/work/acvite_code/test/mfc5j3_appsw_libconvert/build/ubuntu/output/lib/libspi-protocol-convert.so)
==4761==    by 0xCE7A2C5: zmq_socket (in /home/lzy/work/acvite_code/test/mfc5j3_appsw_libconvert/build/ubuntu/output/lib/libspi-protocol-convert.so)
==4761==    by 0xCE633EA: PubCANOutput::InitPub() (spi-service-protocol.h:33)
==4761==    by 0xCE63A93: PubCANOutput::IpcCANOutputPub(unsigned char const*, int) (spi-service-protocol.h:65)
==4761==    by 0xCE60F90: Send (spi-protocol-convert.h:22)
==4761==    by 0xCE60F90: Send (protocol-convert-base.h:49)
==4761==    by 0xCE60F90: HobotADAS::SPIProtocolConvert::OnMessageEnd(long const&, long const&) (spi-protocol-convert.cc:134)
==4761==    by 0x2AE98C: HobotADAS::CommPluginManager::ProcessMsg() (comm_plugin_manager.cpp:321)

分析_示例二

这个报告显示了可能的内存泄漏,这些内存泄漏发生在libspi-protocol-convert.so库中的某些函数调用中。具体来说,这些函数包括zmq::thread_t::start(), zmq::epoll_t::start(), zmq::reaper_t::start(), zmq::ctx_t::create_socket(), zmq_socket, PubCANOutput::InitPub(), PubCANOutput::IpcCANOutputPub(), 和 HobotADAS::SPIProtocolConvert::OnMessageEnd()
这些函数在调用过程中,可能通过callocpthread_create@@GLIBC_2.34分配了内存,但可能没有正确地释放这些内存。
然而,需要注意的是,这些内存泄漏被标记为"可能丢失",这意味着Valgrind不能确定这些内存是否真的泄漏。有时候,这可能是由于复杂的内存管理策略或者特定的库函数(例如,某些线程函数)的行为导致的。
所以,虽然报告显示libspi-protocol-convert.so可能存在内存泄漏,但这并不是确定的。你可能需要进一步检查你的代码,特别是那些涉及到内存分配和释放的部分,以确定是否真的存在内存泄漏。

Massif堆栈检测工具

Massif是Valgrind的一个工具,主要用于分析程序在运行过程中的堆内存使用情况。它可以帮助开发者找出程序在运行过程中消耗过多内存的问题,尤其在程序结束后内存能正常释放,但运行过程中内存持续增长的情况下。

Massif的基本使用

你可以使用以下命令来运行Massif:

valgrind --tool=massif your_program [your_program_arguments]

这将生成一个名为 massif.out.pid 的文件,其中 pid 是你的程序的进程 ID。你可以使用 ms_print 命令来查看这个文件的内容:

ms_print massif.out.pid

这将显示你的程序在运行过程中的内存使用情况。你可以查看这个报告,看看是否有任何意外的内存使用峰值,或者内存使用是否随时间持续增加。

Massif的限制

请注意,Massif只能测量堆内存的使用情况,不能测量栈内存或其他类型的内存。如果你的程序在其他类型的内存上有问题,你可能需要使用其他工具或技术来检测。

Massif的高级使用

虽然Massif可以提供程序整体的内存使用情况,但它并不能直接告诉你哪个模块或哪段代码在持续申请内存。然而,你可以使用一些额外的选项和工具来获取更详细的信息。

使用 --alloc-fn 选项

如果你知道哪个函数在申请内存(例如,如果你的模块有一个特定的内存分配函数),你可以使用 --alloc-fn 选项来告诉 Massif 跟踪这个函数的内存分配:

valgrind --tool=massif --alloc-fn=my_alloc your_program [your_program_arguments]

这将使 Massif 把所有通过 my_alloc 函数分配的内存都计入到 my_alloc 的调用者的名下。

使用 --pages-as-heap 选项

这个选项会让 Massif 把所有的内存页都当作堆来处理,这可以让你看到所有的内存分配,而不仅仅是通过 mallocnew 等函数分配的内存:

valgrind --tool=massif --pages-as-heap=yes your_program [your_program_arguments]

使用 Callgrind 进行性能分析

虽然 Callgrind 主要用于性能分析,但它也可以显示每个函数的内存使用情况。你可以使用 Callgrind 来看哪个函数分配了最多的内存:

valgrind --tool=callgrind your_program [your_program_arguments]

然后你可以使用 kcachegrind 或其他 Callgrind 数据查看器来查看结果。

请注意,这些方法可能需要你对你的代码和它的内存使用模式有一定的理解。如果你的代码很复杂,或者它使用了许多不同的内存分配函数,这可能会比较困难。

查看报告

Massif与其他一些内存检测工具有所不同,它在程序运行过程中就会生成报告,而不是等到程序结束后。

Massif的报告是一个文本文件,文件名通常为massif.out.pid,其中pid是运行程序的进程ID。这个报告文件包含了程序运行过程中的内存使用情况的详细信息。

报告中的每一行都代表一个采样点,显示了在该时刻程序的堆内存使用情况。每个采样点都包含了以下信息:

  • 时间:从程序开始运行到该采样点的时间。
  • 内存使用量:在该采样点,程序使用的堆内存的总量。
  • 堆栈跟踪:在该采样点,程序的堆栈跟踪信息,显示了哪些函数调用导致了内存的分配。

你可以使用ms_print命令来查看和解析这个报告文件:

ms_print massif.out.pid

ms_print会将报告文件的内容格式化并输出到控制台,使其更易于阅读和理解。输出的内容包括一个内存使用情况的图表,以及每个采样点的详细信息。

Callgrind 性能分析工具

Valgrind 的 Callgrind 工具主要是用来收集程序的运行时行为信息,包括函数调用次数、指令读取次数等。然而,它并不直接测量函数的执行时间。虽然指令读取次数和函数调用次数可以提供一些关于程序性能的信息,但它们并不能直接告诉你哪些函数是耗时的。

指令读取次数

"指令读取次数"是一个度量,它表示在程序执行过程中,一个特定函数中的指令被读取(并可能被执行)的次数。这个度量可以帮助你了解哪些函数在你的程序中被频繁地执行。

然而,指令读取次数并不直接等于函数的执行时间。一个函数可能有很多指令,但如果这些指令都很快地执行,那么这个函数的执行时间可能仍然很短。另一方面,一个函数可能只有少量的指令,但如果这些指令需要很长时间来执行(例如,如果它们包含了磁盘 I/O 或网络通信),那么这个函数的执行时间可能会很长。

因此,虽然指令读取次数可以提供一些关于程序性能的信息,但它并不能直接告诉你哪些函数是耗时的。如果你想了解函数的执行时间,你可能需要使用其他的性能分析工具或技术,例如 CPU 采样分析器或计时器。

使用参数

Valgrind 的 Callgrind 工具有许多选项可以用来定制它的行为和输出。以下是一些可能对你有用的选项:

  • --dump-instr=yes:这个选项让 Callgrind 收集每个指令的信息,而不仅仅是每个函数的信息。这可以让你更详细地了解你的程序的行为,但也会使 Callgrind 运行得更慢,并生成更大的输出文件。
  • --collect-jumps=yes:这个选项让 Callgrind 收集程序中的跳转信息。这可以帮助你了解你的程序的控制流,但也会使 Callgrind 运行得更慢,并生成更大的输出文件。
  • --branch-sim=yes:这个选项让 Callgrind 模拟程序的分支预测。这可以帮助你了解你的程序的分支预测效率,但也会使 Callgrind 运行得更慢。

你可以在运行 Callgrind 时添加这些选项,例如:

valgrind --tool=callgrind --dump-instr=yes --collect-jumps=yes your_program [your_program_arguments]

然后,你可以使用 callgrind_annotatekcachegrind 来查看结果。这些工具可以显示 Callgrind 收集的详细信息,包括每个指令的信息和跳转信息。

请注意,这些选项可能会使 Callgrind 运行得更慢,并生成更大的输出文件。你应该只在你需要这些详细信息时使用这些选项。

报告生成

你可以将 callgrind_annotate 的输出重定向到一个文件中。在 Unix-like 系统(如 Linux 或 macOS)中,你可以使用 > 符号来重定向输出。例如:

callgrind_annotate callgrind.out.pid > output.txt

这将会把 callgrind_annotate 的输出写入到 output.txt 文件中,而不是显示在终端上。如果 output.txt 文件已经存在,这个命令会覆盖它的内容;如果文件不存在,这个命令会创建它。

如果你想把输出追加到一个已经存在的文件中,而不是覆盖它,你可以使用 >> 符号:

callgrind_annotate callgrind.out.pid >> output.txt

这将会把 callgrind_annotate 的输出追加到 output.txt 文件的末尾。

请注意,这些命令在 Windows 系统的命令提示符中可能不起作用。如果你在 Windows 系统上运行 Valgrind(例如,通过 WSL 或 Cygwin),你应该在相应的 Unix-like 环境中使用这些命令。

可视化打开报告

kcachegrindqcachegrind 是两个用于可视化性能分析数据的工具,它们可以读取由 Valgrind 的 Callgrind 工具生成的输出,并生成详细的调用图。

以下是如何使用这两个工具的基本步骤:

  1. 生成 Callgrind 的输出:首先,你需要使用 Callgrind 工具来生成性能分析数据。你可以通过以下命令来运行你的程序,并生成 Callgrind 的输出:
    valgrind --tool=callgrind your_program
    
    在这个命令中,your_program 是你要分析的程序。这个命令会生成一个名为 callgrind.out.pid 的文件,其中 pid 是你的程序的进程 ID。
  2. 安装 kcachegrind 或 qcachegrind:然后,你需要安装 kcachegrindqcachegrind。你可以通过你的包管理器来安装这两个工具。例如,如果你使用的是 Ubuntu,你可以通过以下命令来安装 kcachegrind
    sudo apt-get install kcachegrind
    
    或者,你可以通过以下命令来安装 qcachegrind
    sudo apt-get install qcachegrind
    
  3. 打开 Callgrind 的输出:最后,你可以使用 kcachegrindqcachegrind 来打开 Callgrind 的输出。你可以通过以下命令来打开 callgrind.out.pid 文件:
    kcachegrind callgrind.out.pid
    
    或者:
    qcachegrind callgrind.out.pid
    
    在这两个命令中,callgrind.out.pid 是 Callgrind 的输出文件。

kcachegrindqcachegrind 会显示一个详细的调用图,你可以通过这个图来查看你的程序的性能瓶颈。你可以点击图中的节点来查看详细的调用信息,包括调用次数、消耗的 CPU 时间等。

注意:无法生成火焰图

callgrind_annotate 本身并不支持生成火焰图。火焰图是一种可视化工具,用于显示程序的性能分析数据。它们通常用于显示 CPU 使用情况,但也可以用于显示其他类型的性能数据。

gprof2dot 是一个用于生成调用图的工具,它可以接受多种性能分析工具的输出,包括 gprof、oprofile、HProf、Xdebug、Visual Studio、VTune等,但是它并不能直接解析 callgrind_annotate 的输出。

报告分析

性能分析报告片段 示例一

 --------------------------------------------------------------------------------
   2 Profile data file 'callgrind.out.3896' (creator: callgrind-3.18.1)
   3 --------------------------------------------------------------------------------
   4 I1 cache:
   5 D1 cache:
   6 LL cache:
   7 Timerange: Basic block 0 - 3688459443
   8 Trigger: Program termination
   9 Profiled target:  ./comm_plugin_manager (PID 3896, part 1)
  10 Events recorded:  Ir
  11 Events shown:     Ir
  12 Event sort order: Ir
  13 Thresholds:       99
  14 Include dirs:
  15 User annotated:
  16 Auto-annotation:  on
  17
  18 --------------------------------------------------------------------------------
  19 Ir
  20 --------------------------------------------------------------------------------
  21 7,662,006,442 (100.0%)  PROGRAM TOTALS
  22
  23 --------------------------------------------------------------------------------
  24 Ir                      file:function
  25 --------------------------------------------------------------------------------
  26 1,335,146,312 (17.43%)  ./string/../sysdeps/x86_64/multiarch/memset-vec-unaligned-erms.S:__memset_avx2_unaligned_erms [/usr/lib/x86_64-linux-gnu/libc.so.6]
  27 1,255,927,115 (16.39%)  ./string/../sysdeps/x86_64/multiarch/memmove-vec-unaligned-erms.S:__memcpy_avx_unaligned_erms [/usr/lib/x86_64-linux-gnu/libc.so.6]
  28   767,318,807 (10.01%)  ./malloc/./malloc/malloc.c:_int_free [/usr/lib/x86_64-linux-gnu/libc.so.6]
  29   748,924,400 ( 9.77%)  ./malloc/./malloc/malloc.c:_int_malloc [/usr/lib/x86_64-linux-gnu/libc.so.6]
  30   424,929,976 ( 5.55%)  ./malloc/./malloc/malloc.c:malloc [/usr/lib/x86_64-linux-gnu/libc.so.6]
  31   191,346,740 ( 2.50%)  ./malloc/./malloc/malloc.c:free [/usr/lib/x86_64-linux-gnu/libc.so.6]
  32   152,418,873 ( 1.99%)  ./malloc/./malloc/malloc.c:malloc_consolidate [/usr/lib/x86_64-linux-gnu/libc.so.6]
  33   107,482,497 ( 1.40%)  ./string/../sysdeps/x86_64/multiarch/memcmp-avx2-movbe.S:__memcmp_avx2_movbe [/usr/lib/x86_64-linux-gnu/libc.so.6]
  34   105,946,368 ( 1.38%)  ???:operator new(unsigned long) [/usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.30]
  35    96,170,106 ( 1.26%)  ???:hobot::pack_sdk::Meta::GetDataIndex(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, int) const [/home/lzy/work/acvi     te_code/master/mfc5j3_appsw_libconvert/build/ubuntu/output/bin/comm_plugin_manager]
  36    94,600,118 ( 1.23%)  ./malloc/./malloc/arena.c:free
  37    82,278,706 ( 1.07%)  ???:std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::compare(char const*) const [/usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.30]  38    60,595,452 ( 0.79%)  ???:ObstacleProto::WorldSpaceInfo::MergeFrom(ObstacleProto::WorldSpaceInfo const&) [/home/lzy/work/acvite_code/master/mfc5j3_appsw_libconvert/build/ubuntu/outp     ut/lib/libspi-protocol-convert.so]
  39    58,554,462 ( 0.76%)  ./malloc/./malloc/malloc.c:unlink_chunk.constprop.0 [/usr/lib/x86_64-linux-gnu/libc.so.6]
  40    54,806,504 ( 0.72%)  ???:hobot::pack_sdk::Meta::GetTopicMeta(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, int, std::vector<long, std::all     ocator<long> >*, std::vector<long, std::allocator<long> >*, std::vector<void const*, std::allocator<void const*> >*, std::vector<unsigned long, std::allocator<unsigned long> >*) const      [/home/lzy/work/acvite_code/master/mfc5j3_appsw_libconvert/build/ubuntu/output/bin/comm_plugin_manager]
  41    52,638,193 ( 0.69%)  ???:ObstacleProto::Obstacle::MergeFrom(ObstacleProto::Obstacle const&) [/home/lzy/work/acvite_code/master/mfc5j3_appsw_libconvert/build/ubuntu/output/lib/libsp     i-protocol-convert.so]
  42    52,069,931 ( 0.68%)  ???:std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_assign(std::__cxx11::basic_string<char, std::char_traits<char>, std::al     locator<char> > const&) [/usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.30]
  43    47,538,816 ( 0.62%)  ???:PerceptionBaseProto::Point::MergeFrom(PerceptionBaseProto::Point const&) [/home/lzy/work/acvite_code/master/mfc5j3_appsw_libconvert/build/ubuntu/output/lib     /libspi-protocol-convert.so]
  44    46,026,687 ( 0.60%)  ./string/../sysdeps/x86_64/multiarch/strlen-avx2.S:__strlen_avx2 [/usr/lib/x86_64-linux-gnu/libc.so.6]
 

分析片段一

在这个报告中,括号内的百分比表示的是 “Ir”(指令读取)事件的百分比。这是一个度量,表示在程序执行过程中,该函数中的指令被读取(并可能被执行)的次数占总指令读取次数的百分比。

在这个上下文中,“Ir” 代表 “Instruction read”,也就是读取的指令数。这个数字表示在程序执行过程中,处理器读取了多少条指令。

“7,662,006,442 (100.0%) PROGRAM TOTALS” 这一行表示在整个程序执行过程中,总共读取了 7,662,006,442 条指令。

例如,对于以下行:

1,335,146,312 (17.43%)  ./string/../sysdeps/x86_64/multiarch/memset-vec-unaligned-erms.S:__memset_avx2_unaligned_erms [/usr/lib/x86_64-linux-gnu/libc.so.6]

这表示 __memset_avx2_unaligned_erms 函数中的指令被读取了 1,335,146,312 次,这占总指令读取次数的 17.43%。

这个度量可以帮助你了解你的程序的性能瓶颈在哪里。如果一个函数的指令读取次数占比很高,那么这可能意味着这个函数是你的程序的一个热点,你可能需要优化这个函数以提高程序的性能。

请注意,这个度量并不直接表示函数的调用次数或耗时。一个函数的指令读取次数可能会因为它的大小、它被调用的次数、它的代码复杂性等因素而变化。如果你想了解函数的调用次数或耗时,你可能需要使用其他的性能分析工具或技术。

性能分析报告片段 示例二

       .           void IdAdaptor::UpdateMap(
        .             std::map<uint32_t, uint8_t> &current_map_,
        .             std::map<uint32_t, uint8_t> &history_map_,
        .             const std::vector<ObstacleProto::Obstacle>  &object,
        .             uint16_t trackAge[],
   40,766 ( 0.00%)    uint8_t trackAgeSize) {
        .             // clear the current_map_
        .             current_map_.clear();
    2,398 ( 0.00%)    int objects_size = object.size();
        .             std::vector<uint32_t> new_id;  
        .             std::vector<uint8_t> vec_current_id;
   19,184 ( 0.00%)    if ((trackAge == nullptr)|| (objects_size <= 0) || (trackAgeSize <= 0)) {
        .               return;
        .             } 

分析片段二

报告中,括号内的百分比仍然表示的是 “Ir”(指令读取)事件的百分比。这是一个度量,表示在程序执行过程中,该函数中的指令被读取(并可能被执行)的次数占总指令读取次数的百分比。

对于你的函数 IdAdaptor::UpdateMap,它的指令读取次数占总指令读取次数的百分比是 0.00%。这意味着在你的程序执行过程中,这个函数中的指令被读取的次数相对于整个程序的指令读取次数来说是非常小的。

这可能是因为这个函数被调用的次数较少,或者这个函数的代码较短,或者这个函数的执行被优化了。无论哪种情况,这都意味着这个函数可能不是你程序性能瓶颈的地方。

请注意,这个度量并不直接表示函数的调用次数或耗时。一个函数的指令读取次数可能会因为它的大小、它被调用的次数、它的代码复杂性等因素而变化。如果你想了解函数的调用次数或耗时,你可能需要使用其他的性能分析工具或技术。

与其他工具的比较

perfValgrind

perf 和 Valgrind 的 Callgrind 工具都是性能分析工具,但它们在设计和使用上有一些重要的区别。

  1. 数据收集方式perf 是一个基于事件的采样分析器,它定期检查系统的状态(例如,每隔一定数量的 CPU 周期)并记录当前正在执行的函数。这种方法可以提供关于哪些函数在 CPU 上花费了最多时间的信息,但它可能会错过一些短暂但频繁的函数调用。另一方面,Callgrind 是一个基于模拟的分析器,它记录程序的所有函数调用和指令读取,这可以提供更详细的信息,但也可能导致更大的性能开销。
  2. 可用的信息perf 可以收集各种类型的性能事件,包括 CPU 周期、缓存命中/未命中、分支预测错误等。这可以帮助你了解你的程序在硬件层面的行为。另一方面,Callgrind 主要关注程序的行为,例如函数调用和指令读取。它还可以提供一些关于内存使用和数据流的信息。
  3. 易用性和便携性perf 是 Linux 的一部分,它在所有 Linux 系统上都可用,但在其他操作系统上可能不可用。另一方面,Valgrind 是一个独立的工具,它可以在多种操作系统上运行,包括 Linux、macOS 和 Windows(通过 Cygwin 或 WSL)。

总的来说,perf 和 Callgrind 都是强大的工具,它们可以提供不同类型的性能分析信息。选择哪个工具取决于你的具体需求。在某些情况下,你可能会发现同时使用两种工具可以提供最全面的性能分析。

gperfValgrind

gprof 和 Valgrind 是两种不同的性能分析工具,它们在设计和使用上有一些重要的区别。

  1. 数据收集方式gprof 是一个基于采样的分析器,它通过定期中断程序执行并记录当前正在执行的函数来工作。这种方法可以提供关于哪些函数在 CPU 上花费了最多时间的信息,但它可能会错过一些短暂但频繁的函数调用。另一方面,Valgrind 的 Callgrind 工具是一个基于模拟的分析器,它记录程序的所有函数调用和指令读取,这可以提供更详细的信息,但也可能导致更大的性能开销。
  2. 可用的信息gprof 提供了函数调用次数和每个函数的累积执行时间的信息,这可以帮助你了解哪些函数在程序执行中占据了大部分时间。另一方面,Callgrind 提供了更详细的信息,包括每个函数的指令读取次数、缓存命中/未命中次数、分支预测错误次数等。这可以帮助你更深入地了解你的程序的行为和性能瓶颈。
  3. 易用性和便携性gprof 是 GNU binutils 的一部分,它在所有支持 GNU 工具链的系统上都可用。然而,为了使用 gprof,你需要在编译你的程序时加上 -pg 选项,这可能会使你的程序运行得更慢。另一方面,Valgrind 是一个独立的工具,它可以在多种操作系统上运行,包括 Linux、macOS 和 Windows(通过 Cygwin 或 WSL)。使用 Valgrind 不需要修改你的编译选项,但它的性能开销通常比 gprof 更大。

总的来说,gprof 和 Valgrind 都是强大的工具,它们可以提供不同类型的性能分析信息。选择哪个工具取决于你的具体需求。在某些情况下,你可能会发现同时使用两种工具可以提供最全面的性能分析。

对比图表

工具 数据收集方式 可用的信息 易用性和便携性 平台 需要的编译选项
Valgrind 基于模拟的分析器,记录所有函数调用和指令读取 提供详细的信息,包括每个函数的指令读取次数、缓存命中/未命中次数、分支预测错误次数等 可在多种操作系统上运行,包括 Linux、macOS 和 Windows(通过 Cygwin 或 WSL) Linux, macOS, Windows (Cygwin, WSL)
gprof 基于采样的分析器,通过定期中断程序执行并记录当前正在执行的函数 提供了函数调用次数和每个函数的累积执行时间的信息 是 GNU binutils 的一部分,需要在编译时加上 -pg 选项 所有支持 GNU 工具链的系统 -pg
perf 基于事件的采样分析器,定期检查系统的状态并记录当前正在执行的函数 可以收集各种类型的性能事件,包括 CPU 周期、缓存命中/未命中、分支预测错误等 是 Linux 的一部分,只在 Linux 系统上可用 Linux
Intel VTune 基于事件的采样分析器,定期检查系统的状态并记录当前正在执行的函数 提供了详细的性能分析信息,包括函数的执行时间、CPU 缓存命中率、分支预测准确率等 是 Intel 的商业产品,只在支持 Intel CPU 的系统上可用 Windows, Linux, macOS
Visual Studio Profiler 基于事件的采样分析器,定期检查系统的状态并记录当前正在执行的函数 提供了详细的性能分析信息,包括函数的执行时间、CPU 使用率、内存使用等 是 Visual Studio 的一部分,只在 Windows 系统上可用 Windows
Instruments (macOS) 基于事件的采样分析器,定期检查系统的状态并记录当前正在执行的函数 提供了详细的性能分析信息,包括函数的执行时间、CPU 使用率、内存使用等 是 Xcode 的一部分,只在 macOS 系统上可用 macOS

注意事项

优雅的结束进程

不要使用kill -9

使用 kill -9 杀死正在运行的 Valgrind 进程会影响检测结果。kill -9 会立即终止进程,不给它机会进行任何清理工作。在 Valgrind 的情况下,这意味着它可能无法生成完整的报告,因为它通常在被检测的程序正常结束时生成报告。

如果你需要在 Valgrind 运行时停止它,最好的方法是尝试使用 kill 命令(没有 -9 选项)发送一个 TERM 信号。这将请求 Valgrind 优雅地终止,它应该能够生成一个报告,然后退出。

kill <valgrind_pid>

然而,如果 Valgrind 进程由于某种原因无法响应 TERM 信号,你可能需要使用 kill -9。请注意,这可能会导致 Valgrind 无法生成报告,或者生成的报告可能不完整。

总的来说,如果可能,你应该避免在 Valgrind 运行时杀死它,以确保你可以得到完整和准确的报告。如果你需要在 Valgrind 运行时停止它,你应该尽量使用 kill 命令(没有 -9 选项)来优雅地终止它。

Valgrind 的不同工具(如 Memcheck、Callgrind、Massif 等)不能同时运行。每次运行 Valgrind 时,你必须选择一个要使用的工具。这是因为每个工具都有自己的特定目标和方法,它们不能同时应用于同一个程序运行实例。

使用Ctrl+Z无法终止

使用 Ctrl+Z 在命令行中会发送一个 SIGSTOP 信号给当前的前台进程(在这种情况下是 Valgrind)。这会导致进程暂停执行,但不会终止它。你可以使用 fg 命令来恢复暂停的进程。

在 Valgrind 的情况下,使用 Ctrl+Z 并不会直接影响它的检测结果,因为它只是暂停了进程,而不是终止它。然而,如果你在 Valgrind 暂停的时候修改了它正在检测的程序的状态,或者运行了其他可能影响检测结果的操作,那么这可能会影响 Valgrind 的报告。

总的来说,如果你只是想暂时停止 Valgrind,然后稍后继续,使用 Ctrl+Z 是可以的。但是,你应该避免在 Valgrind 暂停的时候进行任何可能影响检测结果的操作。

应该使用killall或kill的默认参数发送TERM 信号

  • kill 命令发送信号到指定的进程 ID。例如,kill 12345 会发送 TERM 信号到进程 ID 为 12345 的进程。

  • killall 命令发送信号到所有名字匹配的进程。例如,killall myprogram 会发送 TERM 信号到所有名为 “myprogram” 的进程。

    如果没有指定信号,killkillall 都默认发送 TERM 信号,这是一个请求进程优雅地终止的信号。你可以使用 -s 选项来指定要发送的信号,例如,kill -s HUP 12345killall -s HUP myprogram

tool工具无法同时使用

Valgrind 的不同工具(如 Memcheck、Callgrind、Massif 等)不能同时运行。每次运行 Valgrind 时,你必须选择一个要使用的工具。这是因为每个工具都有自己的特定目标和方法,它们不能同时应用于同一个程序运行实例。

例如,如果你想进行内存泄漏检测,你应该使用 Memcheck 工具:

valgrind --tool=memcheck --leak-check=yes your_program [your_program_arguments]

如果你想进行性能分析,你应该使用 Callgrind 工具:

valgrind --tool=callgrind your_program [your_program_arguments]

如果你想同时进行内存泄漏检测和性能分析,你需要分别运行两次 Valgrind,一次使用 Memcheck,一次使用 Callgrind。

请注意,虽然这可能需要更多的时间,但这样可以确保你得到的结果是准确和可靠的。如果你试图同时运行两个工具,可能会导致结果的混淆和不准确。

结语

在我们的编程学习之旅中,理解是我们迈向更高层次的重要一步。然而,掌握新技能、新理念,始终需要时间和坚持。从心理学的角度看,学习往往伴随着不断的试错和调整,这就像是我们的大脑在逐渐优化其解决问题的“算法”。

这就是为什么当我们遇到错误,我们应该将其视为学习和进步的机会,而不仅仅是困扰。通过理解和解决这些问题,我们不仅可以修复当前的代码,更可以提升我们的编程能力,防止在未来的项目中犯相同的错误。

我鼓励大家积极参与进来,不断提升自己的编程技术。无论你是初学者还是有经验的开发者,我希望我的博客能对你的学习之路有所帮助。如果你觉得这篇文章有用,不妨点击收藏,或者留下你的评论分享你的见解和经验,也欢迎你对我博客的内容提出建议和问题。每一次的点赞、评论、分享和关注都是对我的最大支持,也是对我持续分享和创作的动力。


阅读我的CSDN主页,解锁更多精彩内容:泡沫的CSDN主页
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_21438461/article/details/131668820