C++相关工具盘点

盘点各种C++开发中可能使用到的工具及其详细使用方式

参考链接

代码性能分析

代码性能分析乃是分析程序,用以从执行时间、内存使用、函数调用以及其他指标等方面来衡量其性能的一个过程。这有助于我们去识别:

  • 热点:消耗 CPU 时间最多的函数和代码块
  • 频率:递归调用次数异常多的函数
  • 低效内存使用:存在内存泄漏或过度分配的区域
  • 缓慢的函数或者循环:一般来讲,那些得进行优化从而提高整体速度的函数。

这种分析使开发者能够明确,哪些部分对应用程序性能的拖累最大,从而集中精力进行优化,而不是随意进行更改。

大体上来说,有两类主要的性能分析器,它们各自具有不同的优势和适用场景:

  • 采样型性能分析器:周期性捕获程序状态快照,轻量级且开销极小,适合长时间运行的应用程序。但对于生命周期很短的函数,可能无法提供准确的信息。例如“Very Sleepy”、谷歌性能分析工具(gperftools)、Visual Studio 性能分析器以及英特尔 VTune 放大器。
  • 插桩型性能分析器:为每个函数调用和代码块捕获详细且准确的数据,提供更精确结果,但开销较高,不适合生产环境。例如包括 GNU 性能分析器(gprof)、Callgrind、Tracy、Remotery 和 Microprofile。

数据颗粒度,是指在进行性能分析之时所捕获数据的详细程度。不同工具,提供不同级别的数据颗粒度,这对于理解工具选择以及适用场景,非常有帮助。例如:

  • 高颗粒度:插桩型工具一般会给出,行级别的数据,所以开发者可以清楚地知晓,每一行代码的执行时间。如此一来,在识别特定函数内部的问题时,就变得非常关键了。
  • 低粒度:采样型工具给出相对来说比较低粒度的数据,重点把注意力放在函数级别的信息上面。这种办法对于长时间运转的应用程序是合适的,不过呢可能没办法及时察觉到短生命周期函数里存在的问题。

无论使用什么分析工具,重点关注下面的建议:

热点分析:关注消耗时间最多函数或循环。

调用图分析:就是要弄明白函数调用的层级关系,与此同时把效率低的调用路径给找出来。

内存性能分析:检查堆分配、内存泄漏以及不必要对象拷贝。除此之外,要确保性能分析是在实际工作负载下进行,而不是在虚拟场景中。

测试增量变更:每次改完之后,都得做性能测试,保证相应的改进能实现。

确认正确的代码:需保证所分析的是准确且未被优化过的代码。若编译器进行了,优化,我们或许会见到不正确的结果示例。认真验证代码的正确性,就能避免因编译器优化而致的误导。一旦编译器执行了优化操作,就有出现与预期不同结果的可能。

多种工具一起用:有的时候,单单一种办法可能不够,所以呢可以试试多种工具,这样就能全方位地了解代码行为啦。

C++分析工具

并非所有性能分析器都能在所有平台上运行。例如 gprof 和 Callgrind 仅在 Linux 环境中可用,而 Visual Studio 性能分析器仅适用于 Windows。

AddressSanitizer

AddressSanitizer是Google旗下的一个内存问题检测工具,项目地址

Sanitizers中的内存错误检测工具是AddressSanitizer (ASan)AddressSanitizer又集成了内存泄露检测工具LeakSanitizer(LSan)。因此使用Sanitizers工具检测内存错误,主要就是使用AddressSanitizer。

与传统的内存问题检测工具,如 Valgrind,最大的区别是效率:

  • Valgrind 大约降低10倍运行速度
  • AddressSanitizer 大约降低2运行速度
Valgrind Sanitizers
平台支持 Linux Linux、macOS、Windows(部分功能)
原理 动态二进制插桩 编译时插桩
重新编译 不需要 需要
运行减速 20X – 50X 2X–4X
堆越界 支持 支持
栈越界 不支持 支持
返回后栈使用 不支持 支持
释放重引用 支持 支持
未定义行为 不支持 支持
内存未初始化 支持 支持

在LLVM及高版本编译器中已经自带了该工具,编译时添加 -fsanitize=address 选项。
正常运行程序,如有内存相关问题,即会打印异常信息。

原理参考官方文档 原理讲解

  • AddressSanitizer(ASan): 内存地址越界检查

    -fsanitize=address

  • LeakSanitizer(LSan): 内存泄漏检查,可以单独使用

    -fsanitize=leak

    目前只支持linux

  • ThreadSanitizer (TSan): 线程安全检查

    检测多线程程序中的数据竞争和死锁问题

    -fsanitize=thread

  • UndefinedBehaviorSanitizer (UBSsan): 未定义行为检查

    比如整数溢出,除以零等

    -fsanitize=underfined

  • MemorySanitizer (MSan): 内存分配检查

    检测使用未初始化的内存

以上这些工具都是clang/clang++编译器自带的,他们位于llvm项目的一个子项目Sanitizer中。因此,如果我们使用clang编译器,便可以很方便地使用这些功能。需要注意的是,Mac自带的clang编译器和llvm的clang的编译器,稍有不同,Mac自带的clang不支持LeakSanitizer的内存泄漏检查功能

优势

  • 不需要额外安装,比如在Mac下安装valgrind,并不是十分方便
  • 可以无缝接入到cmake,可以在CMakeLists.txt中直接设置编译选项
  • 运行速度快,个人实际使用体验,比valgrind快太多了

-fsanitize=address -g -O1 -fno-omit-frame-pointer

  1. -g
    该选项告诉编译器生成调试信息,这对于在发生内存错误时获取有用的堆栈跟踪和源代码位置信息非常重要。
  2. -O1
    这是一个优化级别选项,-O1表示进行基本的优化,可以提高程序性能,同时保留足够的调试信息。更高级别的优化(如-O2-O3)可能会使调试信息不准确或丢失。
  3. -fno-omit-frame-pointer
    默认情况下,编译器可能会优化掉帧指针(frame pointer),以减小二进制大小和提高性能。但这会使堆栈跟踪信息不完整。-fno-omit-frame-pointer选项可以确保保留帧指针,从而获得更准确的堆栈跟踪。

原理

编译时插桩(Compile-time Instrumentation,CTI)

ASan通过编译时的插桩和运行时的动态检查,来检测和调试内存相关的错误。它在编译期间会为每个内存分配和释放操作添加额外的代码,确保在运行过程中对内存的每次读写都经过ASan的验证,从而发现内存问题。

数据区域:可访问区域和不可访问区域 (redzone)

ASan会为每个内存块设置边界标记。每次内存分配时,ASan在正常的可用内存区域周围插入一些不可访问的区域,称为 redzone。这些区域主要用于检测越界访问。可访问区域 是正常分配的内存空间,供程序读取和写入数据。不可访问区域 (redzone) 是在内存块两侧添加的一些填充区域,作为缓冲区,防止越界访问。redzone 的存在可以捕获那些访问未分配或已经释放的内存操作。

影子内存 (Shadow Memory)

影子内存用于跟踪主内存的可访问性状态。影子内存和正常内存的比例是 1:8,即每 1 字节的影子内存可以描述 8 字节的正常内存。这意味着,影子内存中的每个字节对应主内存中的 8 个字节,用于标记这些字节是否可访问。如果主内存的某一部分是可访问的,影子内存中的相应位就会被标记为“可访问”。如果主内存的某一部分处于 redzone,影子内存中的相应位就会被标记为“不可访问”。在每次内存访问时,ASan会查询影子内存以判断该访问是否合法。如果访问的是不可访问区域,ASan会立即报告错误并提示开发者。

gperftools

开源地址

工具 使用命令 是否需要重新编译 Profiling速度 是否支持多线程热点分析 是否支持链接库热点分析
gprof ./test; gprof ./test ./gmon.out
valgrind Valgrind --tool=callgrind ./test 非常慢
gperftools LD_PRELOAD=/usr/lib/libprofiler.so CPUPROFILE=./test.prof ./test

安装: brew install gperftools

不会使用,暂略

arm暂时不支持

perf

Linux平台专属

通常情况下,在大多数Linux发行版中,perf工具是随Linux内核一起提供的,并且应该在包含在linux-tools包中。但是,有时在一些定制或精简的内核版本中可能没有预安装perf工具

开源地址

系统级性能优化通常包括两个阶段:性能剖析(performance profiling)和代码优化。性能剖析的目标是寻找性能瓶颈,查找引发性能问题的原因及热点代码。代码优化的目标是针对具体性能问题而优化代码或编译选项,以改善软件性能。本篇主要讲性能分析中常用的工具——perf

Perf的原理:

  1. Perf是内置于Linux内核源码树中的性能剖析(profiling)工具。它基于事件采样原理,以性能事件为基础,支持针对处理器相关性能指标与操作系统相关性能指标的性能剖析。常用于性能瓶颈的查找与热点代码的定位。
  2. 通过它,应用程序可以利用 PMU,tracepoint 和内核中的特殊计数器来进行性能统计。
  3. 使用 perf可以分析程序运行期间发生的硬件事件,比如 cache miss等;也可以分析软件事件,比如 page fault 和进程

上述部分名词介绍:

  1. PMU:性能监控单元(Performance Monitor Unit), CPU提供的一个性能监视单元,用于统计CPU性能数据;
  2. Tracepoint:散落在内核源代码中的一些 hook,它们可以在特定的代码被运行到时被触发,这一特性可以被各种 trace/debug 工具所使用。
  3. 内核运行状态计数,例如: 1) 进程切换 2) Page fault 3) 中断计数

安装

由于和内核的紧密关系,perf 的安装需要与内核版本相匹配,一般来讲使用发行版自带的包管理器安装即可,注意不同发行版下的包名称:

  • Alpine: perf,v3.12 以上才可安装
  • Debian: linux-perf,注意 Debian 10 的软件源中默认只有 4.19 版本,若需 5.10 版本,可使用 buster-backports 源
  • Ubuntu: linux-tools-*,星号为内核版本号或 generic

如果确实无法用包管理器安装或版本不匹配,可以下载对应版本内核源码并解压,在 tools/perf 目录下自行编译。

安装好 perf 后,可以用 perf --helpman perf 查看相应的帮助信息,下面仅介绍使用 perf 对应用进程进行分析的基本流程。

1
2
3
4
5
6
7
# Ubuntu系统
sudo apt-get install linux-tools-common linux-tools-generic linux-tools-`uname -r`
# Centos系统
yum install -y perf

#查看perf版本
perf --verion

注意,由于Docker容器的设计原理所导致,Docker容器是无法直接安装perf工具的

docker中如何使用perf参考

附脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/bin/bash

set -x

target_container_id="$1"
version="$(uname -r)"
version="${version%%-*}"
version="${version%.*}"
tag="v${2-$version}"
if [[ $tag != "v5.4" && $tag != "v5.10" ]]; then
tag="latest"
fi
image=${3-"perf"}

docker run \
--cap-add CAP_SYS_ADMIN \
--privileged \
-ti \
--rm \
--pid=container:$target_container_id \
--network=container:$target_container_id \
$image:$tag

复制以上代码保存为 attach.sh,执行 attach.sh <目标容器ID> 就进入了 perf 容器,此时可以使用 ps 查看目标容器中的进程,记下 pid 后执行 record.sh <pid> 开始记录,记录完成后运行 plot.sh <图片名.svg> 生成火焰图。

使用

使用perf对系统 CPU 事件做采样

1
2
3
4
# 方式一:对一个正在运行的进程,进行采样
perf record -p PID -g -- sleep 60
# 方式二:全新运行一个二进制文件main,进行采样
sudo perf record -F 99 -g ./main -- sleep 60

生成火焰图

利用这个开源工具可以将报告生成可视化的svg图片,更容易查看对应的CPU开销时间和调用栈深度。

1
2
3
4
5
git clone --depth 1 https://github.com/brendangregg/FlameGraph.git
# 安装perl
yum install -y perl
# Ubuntu
apt install -y perl

生成火焰图的脚本

对二进制文件main进行10秒的采样,然后生成火焰图

非root用户需要加sudo

1
2
3
4
perf record -g ./main  sleep 10
perf script -i perf.data &> perf.unfold
./FlameGraph/stackcollapse-perf.pl perf.unfold &> perf.folded
./FlameGraph/flamegraph.pl perf.folded > perf.svg

将火焰图从docker导出到本地

1
docker cp 3cf72755eea5:/home/tmp/perf.svg /Users/yaojun/Desktop

valgrind

速度慢但是最强大

不支持arm架构

可以在arm mac上转译运行他,用于检测x86架构的程序

[[mac及linux_C++环境#mac上部署docker开发环境|参考此处]]

vs自带性能分析工具

使用Visual Studio的诊断工具可以帮助你快速定位到C#内存泄露的代码。以下是一些步骤1234

  1. 启动诊断工具:首先,你需要将你的程序在Visual Studio中运行起来,然后打开诊断工具。你可以通过顶部菜单的调试 -> 窗口 -> 显示诊断工具来打开它24
  2. 启用堆分析:在诊断工具窗口中,切换到内存使用率选项卡,然后启用堆分析24
  3. 截取快照:在你的程序运行到某个特定状态后,点击截取快照。这会记录下当前所有在堆上分配的对象的信息24
  4. 执行可能导致内存泄露的操作:在你的程序中执行可能会导致内存泄露的操作,然后再次截取快照13
  5. 比较快照:在诊断工具中,你可以选择两个快照进行比较。这会显示出在这两个时间点之间新分配的对象13
  6. 查看详细信息:你可以查看新分配对象的详细信息,包括它们的类型,大小,以及分配它们的代码的位置24

Coverity

Coverity —— 企业级静态分析工具

BullseyeCoverage —— C/C++代码覆盖率分析工具

umdh

专属于windows下

Windbg工具中的udmh是User Mode Dump Heap的缩写,它是用于分析用户模式下的内存转储文件的工具。udmh工具可以帮助开发人员分析应用程序的内存使用情况,识别内存泄漏和其他内存相关问题。您可以使用udmh工具来查看内存中分配的对象以及它们之间的关系,从而更好地了解应用程序的内存使用情况。

使用方法微软参考

用法步骤参考

第三方开发的图形化小工具

Visual Leak Detector

官方在vs2017停止更新,因此更新版的vs想要使用参考该视频教程

用法参考

gprof

使用方式,运用“-pg”这个标志,接着借助“g++”去对“main”进行编译,执行命令:g++ -o0 -pg main.cpp -o main,要查看性能分析结果,只需运行:gprof main gmon.out

为了更好的实现可视化,推荐运用gprof2dot工具,能把gprof的输出转换为点图(dot graph)

使用命令gprof main gmon.out | gprof2dot -s -w | dot -Tpng -o output.png可以生成执行图,突出显示在每个函数/代码块上花费的时间

Instruments

xcode专用调试工具

检测项目 英文名称
内存泄漏检测 Leaks
CPU使用情况分析 Time Profiler
磁盘活动监控 File Activity
网络活动监控 Network Activity
GPU渲染性能分析 Core Animation
OpenGL ES驱动分析 OpenGL ES Driver
能源消耗分析 Energy Log
系统跟踪 System Trace
自动化UI测试 Automation
僵尸对象检测 Zombies
核心数据检测 Core Data
响应时间分析 Responsiveness
内存分析 Allocations
虚拟内存分析 VM Tracker
活动监视器 Activity Monitor
img

他同样支持命令行操作,但默认未添加到PATH中

单元测试工具

gtest

开源地址

Google Test(简称为gtest)是一个流行的C++测试框架,用于编写和执行单元测试、集成测试和功能测试。它是 Google 开发的开源项目,旨在提供简单、灵活和可扩展的测试解决方案。以下是对 Google Test 的一些重要特点和功能的介绍

  1. 易于入门和使用:Google Test 提供了简洁而直观的 API,使得编写和运行测试用例非常容易。它遵循 xUnit 风格的测试框架设计,并提供了丰富的断言宏来验证预期结果。
  2. 支持多种测试类型:Google Test 支持单元测试、集成测试和功能测试。你可以使用它来编写针对函数、类、模块或整个应用程序的测试。
  3. 参数化测试:Google Test 允许你使用参数化测试来覆盖不同的输入和参数组合。你可以使用 TEST_P 和 INSTANTIATE_TEST_SUITE_P 宏来定义和实例化参数化测试。
  4. 固件(Fixture)支持:Google Test 支持测试固件的概念,允许你在测试之前和之后设置和清理共享资源。通过使用 TEST_F 宏定义测试固件,可以方便地在多个测试用例之间共享初始化和清理代码。
  5. 丰富的断言:Google Test 提供了丰富的断言宏来验证预期结果。例如,你可以使用 EXPECT_EQ 来检查两个值是否相等,或使用 EXPECT_TRUE 来验证条件是否为真。
  6. 输出详细信息:Google Test 在测试运行过程中会生成详细的输出信息,包括测试结果、失败原因和附加信息等。这些信息有助于诊断问题和快速修复错误。
  7. 可扩展性:Google Test 具有良好的可扩展性,允许你编写自定义的测试扩展和辅助函数。你可以根据需要创建自己的断言宏、打印函数和参数生成器等。
  8. 平台支持:Google Test 支持多种平台和编译器,包括 Windows、Linux、macOS 和各种 C++ 编译器。

环境配置

Linux

1
2
3
4
# apt安装
sudo apt install libgtest-dev
# 编译运行
g++ -o main main.cpp test.cpp -lgtest -lgtest_main -pthread && ./main

使用FetchContent_Declare直接在cmake中完成下载与使用

官方文档入门

catch2

开源地址

官方文档中的引入指导

优势: 使用简单依赖少,使用现代C++,内含benchmark

引入brenchmark

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <catch2/catch_test_macros.hpp>
#include <catch2/benchmark/catch_benchmark.hpp>

#include <cstdint>

uint64_t fibonacci(uint64_t number) {
return number < 2 ? number : fibonacci(number - 1) + fibonacci(number - 2);
}

TEST_CASE("Benchmark Fibonacci", "[!benchmark]") {
REQUIRE(fibonacci(5) == 5);

REQUIRE(fibonacci(20) == 6'765);
BENCHMARK("fibonacci 20") {
return fibonacci(20);
};

REQUIRE(fibonacci(25) == 75'025);
BENCHMARK("fibonacci 25") {
return fibonacci(25);
};
}

doctest

最简单的测试框架,单头文件

开源地址

clion对doctest的支持比gtest更好

包管理工具

vcpkg

[[C++基础#vcpkg包管理器|vcpkg包管理器相关知识点]]

网络万能工具

Mongoose

Mongoose 是一个 C/C++ 网络库。它实现了事件驱动的非阻塞 API,适用于 TCP、UDP、HTTP、WebSocket 和 MQTT

开源地址 官方文档

引入方式就是将mongoose.c和mongoose.h两个文件放到项目中通过 #include "mongoose.h"

比如说开一个web服务器,可以使用下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static void fn(struct mg_connection *c, int ev, void *ev_data) {
if (ev == MG_EV_HTTP_MSG) {
struct mg_http_message *hm = (struct mg_http_message *) ev_data;
mg_http_reply(c, 200, "Content-Type: text/plain\r\n", "Hello, world!\n");
}
}

int main()
{
struct mg_mgr mgr;
mg_mgr_init(&mgr);
mg_http_listen(&mgr, "http://0.0.0.0:8000", fn, &mgr);

printf("Starting server on http://localhost:8000\n");
for (;;) mg_mgr_poll(&mgr, 1000);
mg_mgr_free(&mgr);
return 0;
}