C++工程实践必备技能

news2024/11/17 7:29:49

文章目录

    • 单元测试框架
      • 如何引入
      • 如何使用
        • 测试相关
          • SUBCASE
          • TEST_SUITE
          • TEST_CASE_FIXTURE
          • TEST_CASE_TEMPLATE
        • 断言相关
          • 常用断言宏
          • 常用工具函数
    • benchmark框架
      • 如何引入
      • 如何使用
        • 防止被优化
        • 优化不稳定
        • 比较测试结果
        • 计算BigO
        • 输出结果到其他格式
    • CLion中查看测试覆盖率
    • CLion中使用sanitizers检测内存错误
      • 环境准备
      • 如何使用
        • 内存泄漏检测(leak)
        • 内存访问错误检测(address)
        • 多线程数据竞争检测(thread)
    • CLion中使用perf生成火焰图
      • 环境准备
      • 如何使用

单元测试框架

google test是一个C++中常用且历史悠久的测试框架,其他类似且较新的测试框架有 catch2 或 doetest等,这两个测试框架的优势在于引入简单,是完全 head only 的,但是也正是因为 head only 导致编译速度很慢,当然 doctest 还是挺快的,但 catch2 真的编译太慢了。而 googletest 引入就需要我们自行编译了,当然用cmake的话是可以简化这个过程的,gtest 的使用和引入其实也很简单,由于是直接链接编译好的库,所以编译速度是比较快的(最近测试了下,同样以链接库的方式比 doctest 慢一些… )。我现在更推荐使用 doctest 而不是 gtest 了。

本来是想讲 Google test 的使用的(看我开篇就知道),但是使用了 doctest 后,我现在完全放弃了 Googletest,对我而言有以下几点非常好:

  1. 文件轻量,非常轻量,就几个文件,而且代码量好像就7000行,编译速度奇快,而相对的 Googletest 里包含的东西有点多,比如 gmock,对比起来略显重量级。
  2. CLion 对 doctest 的支持更好,每次我用 Googletest 的时候,CLion都需要重新我当前使用的测试框架,而使用doctest后,则完全没有这方面困扰,反应奇快,这也是轻量带来的好处。
  3. api超级友好,用过就真的回不去。虽然断言宏不是很多,但核心观点是它分解了比较表达式,所以会比其他框架用起来方便太多。
  4. 功能丰富(比如支持对模板进行批量测试),尽管代码轻量,但是功能也毫不含糊,感觉比googletest更好用。

如何引入

正如上述所说,doctest 是head-only的,所以仅仅只需要一个 .h 文件即可,但是我建议不要这样,这样编译速度会慢一些,建议使用编译库再链接的方式,这种方式在cmake里面也很简单,如果不懂cmake,可以看看我这期视频:cmake入门 。

你只需在cmake项目中添加下列代码:

include(FetchContent)
FetchContent_Declare(
        doctest
        GIT_REPOSITORY https://github.com/doctest/doctest.git
        GIT_TAG v2.4.9
        GIT_SHALLOW TRUE
)
FetchContent_MakeAvailable(doctest)

target_link_libraries(target doctest_with_main)

这里的仓库链接由于是GitHub上,如果你不会科学上网的话,建议可以去手动下载GitHub上的代码然后 add_subdirectory() 也是一样的。当然也可以把对应的仓库在gitee上创建一个镜像,那么你就可以直接把上面的 GIT_REPOSITORY 换成你镜像的地址了,比如我拉了一个镜像地址如下:https://gitee.com/acking-you/doctest.git 替换即可。

如何使用

开始前,你可以直接去看官方文档,写的也挺详细:官方文档

首先,我们要清楚,一个测试框架,你需要注意的就只有两点:

  1. 如何组织测试 -> 测试宏
  2. 如何进行测试断言 -> 断言宏

通过下面这个简单的测试进行一个简单的讲解:

//这个宏如果是通过链接的方式引入库的话千万不要加,如果是通过直接的include头文件引入的则需要加入
//#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
#include "doctest.h"

int factorial(int number) { return number <= 1 ? number : factorial(number - 1) * number; }

TEST_CASE("testing the factorial function") {
    CHECK(factorial(1) == 1);
    CHECK(factorial(2) == 2);
    CHECK(factorial(3) == 6);
    CHECK(factorial(10) == 3628800);
}

上述代码是在测试斐波那契数列的值。

  1. 通过 TEST_CASE 这个宏来组织一个测试,参数是该测试的名字是一个字符串值,在CLion中会以这个名字来标识这个测试。与 googletest 相比,对应的是 TEST 宏,但不同的是 googletest 需要传两个参数,两个都不是字符串,而且必须符合C++变量命名的字符规则,所以不能以空格或者其他非字母数字的任何符号放在其中,这点其实很不方便。

  2. 通过 CHECK 宏来进行断言判断,如果失败了CLion中会有对应的提示。参数是一个判断表达式,不要小看这个宏,它是默认支持几乎所有内置的类型,并且包括stl容器。对应的 googletest 一般使用 EXPECT_EQ() 传递两个参数来进行比较,默认不支持 const char* 类型,需要使用 EXPECT_STREQ ,而 doctest 则不需要有这方面的考虑,只需要关注这个 CHECK 宏即可,当然它也有对应的 CHECK_XX 宏。

测试相关

经过一个小demo的讲解,那么大家对于测试宏有了一定的了解,下面将继续介绍更多的测试宏。

SUBCASE

这个宏用于在TEST_CASE中继续产生更小的分组,然后你可以安全的捕获到外界的变量来使用。因为每个SUBCASE都是完全独立的重新执行,而不是在同一次执行,比如我将下面的代码块分为1、2、3,那么第一个SUBCASE的顺序将会是 1->2->结束 ,第二个SUBCASE的执行顺序将会是 1->3->结束 。如果最外层的代码在 SUBCASE 后面,那么不会被执行,所有的 SUBCASE 执行情况,我们可以看作是从一个树的根节点到子节点的简单遍历,但每次遍历没有前后文关系(也就是每次遍历都是重新执行的)

TEST_CASE("vectors can be sized and resized") {
    std::vector<int> v(5);
	//1
    REQUIRE(v.size() == 5);
    REQUIRE(v.capacity() >= 5);

    SUBCASE("adding to the vector increases it's size") {
        //2
        v.push_back(1);

        CHECK(v.size() == 6);
        CHECK(v.capacity() >= 6);
    }
    SUBCASE("reserving increases just the capacity") {
        //3
        v.reserve(6);

        CHECK(v.size() == 5);
        CHECK(v.capacity() >= 6);
    }
}

例如下面这个例子将会输出:

TEST_CASE("lots of nested subcases") {
    cout << endl << "root" << endl;
    SUBCASE("") {
        cout << "1" << endl;
        SUBCASE("") { cout << "1.1" << endl; }
    }
    SUBCASE("") {   
        cout << "2" << endl;
        SUBCASE("") { cout << "2.1" << endl; }
        SUBCASE("") {
            cout << "2.2" << endl;
            SUBCASE("") {
                cout << "2.2.1" << endl;
                SUBCASE("") { cout << "2.2.1.1" << endl; }
                SUBCASE("") { cout << "2.2.1.2" << endl; }
            }
        }
        SUBCASE("") { cout << "2.3" << endl; }
        SUBCASE("") { cout << "2.4" << endl; }
    }
}

output

TEST_SUITE

test suite表示测试集,顾名思义,就是可以把 test case 分组。

比如可以这样写:

TEST_SUITE("math") {
    TEST_CASE("") {} // part of the math test suite
    TEST_CASE("") {} // part of the math test suite
}

也可以分开用 TEST_SUITE_BEGIN 和 TEST_SUITE_END 宏来实现:

TEST_SUITE_BEGIN("utils");

TEST_CASE("") {} // part of the utils test suite

TEST_SUITE_END();

TEST_CASE("") {} // not part of any test suite

分组后的好处当然是可以直接分组执行了。

TEST_CASE_FIXTURE

这个宏是用来直接测试某个类的方法的,相当于是通过继承的方式创建了一个新的类,所以 protect 修饰的东西都能访问,比如:

class UniqueTestsFixture
{
 private:
	static int uniqueID;

 protected:
	int conn;

 public:
	UniqueTestsFixture()
		: conn(10)
	{
	}

 protected:
	static int getID()
	{
		return ++uniqueID;
	}
};

int UniqueTestsFixture::uniqueID = 0;

TEST_CASE_FIXTURE(UniqueTestsFixture, "test get ID")
{
	REQUIRE(getID() == conn);
}
TEST_CASE_TEMPLATE

这个宏是用来测试模板的,如果需要测试的模板功能有共通性,只是类型不一致,那么你可以减少重复劳动,直接用这个宏来帮忙实例化再测试。

比如下列代码测试了 std::any 对于接收字符串类型和整数类型的情况测试:

TEST_CASE_TEMPLATE("test std::any as integer", T, char, short, int, long long int) {
	auto v = T();
	std::any var = T();
	CHECK(std::any_cast<T>(var)==v);
}

TEST_CASE_TEMPLATE("test std::any as string", T, const char*, std::string_view, std::string) {
	T v = "hello world";
	std::any var = v;
	CHECK(std::any_cast<T>(var)==v);
}

也可用 TEST_CASE_TEMPLATE_DEFINE 先定义一个模板测试,后面再用 TEST_CASE_TEMPLATE_INVOKE 来决定实例化模板的类型:

TEST_CASE_TEMPLATE_DEFINE("test std::any as integer", T,integer) {
	auto v = T();
	std::any var = T();
	CHECK(std::any_cast<T>(var)==v);
}

TEST_CASE_TEMPLATE_DEFINE("test std::any as string", T,string) {
	T v = "hello world";
	std::any var = v;
	CHECK(std::any_cast<T>(var)==v);
}

TEST_CASE_TEMPLATE_INVOKE(integer, char, short, int, long long int);
TEST_CASE_TEMPLATE_INVOKE(string, const char*, std::string_view, std::string);

断言相关

doctest的断言宏是很有规律的,它的设计我之前也提到过,是一种尽量以表达式的方式去简化对api的记忆,你只需要清楚三个断言的等级即可,当然如果想要直接通过对应的类似于 gtestEXPECT_XXX 之类的api来进行断言,实际上也是有的。

断言宏一共有以下三个等级:

  • REQUIRE:这个等级算是最高的,如果断言失败,不仅会标记为测试不通过,而且会强制退出测试(也就是后续的测试将不会再进行)。
  • CHECK:如果断言失败,标记为测试不通过,但不会强制退出(也就是后续的测试还是会执行)。
  • WARN:如果断言失败,不会标记测试不通过,也不会强制退出,但是会给出对应的提示。
常用断言宏

下面为常见的宏使用,使用这些宏比直接使用表达式的编译速度要快一点。

<LEVEL> 表示 REQUIRE、CHECK、WARN 三个等级。

  • <LEVEL>_EQ(left, right) - same as <LEVEL>(left == right)
  • <LEVEL>_NE(left, right) - same as <LEVEL>(left != right)
  • <LEVEL>_GT(left, right) - same as <LEVEL>(left > right)
  • <LEVEL>_LT(left, right) - same as <LEVEL>(left < right)
  • <LEVEL>_GE(left, right) - same as <LEVEL>(left >= right)
  • <LEVEL>_LE(left, right) - same as <LEVEL>(left <= right)
  • <LEVEL>_UNARY(expr) - same as <LEVEL>(expr)
  • <LEVEL>_UNARY_FALSE(expr) - same as <LEVEL>_FALSE(expr)

小提示:在引入头文件之前定义 DOCTEST_CONFIG_SUPER_FAST_ASSERTS 这个宏,也可以提升编译速度。

<LEVEL>_MESSAGE :这个宏用于在错误的适合你可以设置对应的提示信息。

同样,你可以为了方便,先通过 INFO 宏来进行提示消息的预设,然后只要出现测试失败,都会提示这个预设的消息。

CHECK_MESSAGE(2==1,"not valid");

比如上面的代码可以用 INFO 宏,写成下面这样:

INFO("not valid")
CHECK(2==1);
常用工具函数

doctest::Contains() 用于判断字符串是包含这其中的字符。

比如下面这个例子:

REQUIRE("foobar" == doctest::Contains("foo"));

doctest::Approx() 用于更精确的比较浮点数。

比如下面这个例子:

REQUIRE(22.0/7 == doctest::Approx(3.141).epsilon(0.01)); // allow for a 1% error

benchmark框架

关于benchmark,我建议使用 nanobench ,同样也是因为引入简单轻量,使用简单且 head only

官方文档如下:https://nanobench.ankerl.com/tutorial.html#usage

如何引入

其实官方文档已经介绍了如何引入,它也是推荐使用下面的方式进行引入:

cmake_minimum_required(VERSION 3.14)
set(CMAKE_CXX_STANDARD 17)

project(
    CMakeNanobenchExample
    VERSION 1.0
    LANGUAGES CXX)

include(FetchContent)

FetchContent_Declare(
    nanobench
    GIT_REPOSITORY https://github.com/martinus/nanobench.git
    GIT_TAG v4.1.0
    GIT_SHALLOW TRUE)

FetchContent_MakeAvailable(nanobench)

add_executable(MyExample my_example.cpp)
target_link_libraries(MyExample PRIVATE nanobench)

如何使用

使用非常简单,不依赖于宏,而是使用对应的类的成员函数。

比如:

#include <nanobench.h>

#include <atomic>

int main() {
    int y = 0;
    std::atomic<int> x(0);
    ankerl::nanobench::Bench().run("compare_exchange_strong", [&] {
        x.compare_exchange_strong(y, 0);
    });
}

输出如下:

图片10

可以看得出来,上述的输出结果其实可以直接copy到markdown中,会被渲染为表格。

  • ns/op:每个bench内容需要经历的时间(ns为单位)。
  • op/s:每秒可以执行多少次操作。
  • err%:运行多次测试的波动情况(误差)。
  • ins/op:每次操作需要多少条指令。
  • cyc/op:每次操作需要多少次时钟周期。
  • bra/op:每次操作有多少次分支预判。
  • miss%:分支预判的miss率。
  • total:本次消耗的总时间。
  • benchmark:对应的名字。

对于不同的机器上述的指标支持程度略有不同,官方的描述为:

CPU statistics like instructions, cycles, branches, branch misses are only available on Linux, through perf events. On some systems you might need to change permissions through perf_event_paranoid or use ACL.

防止被优化

如下示例:

#include <nanobench.h>
#include <thirdparty/doctest/doctest.h>

TEST_CASE("tutorial_fast_v1") {
    uint64_t x = 1;
    ankerl::nanobench::Bench().run("++x", [&]() {
        ++x;
    });
}

可能无法输出结果,因为x被优化了,所以可以改为下面这样:

#include <nanobench.h>
#include <doctest/doctest.h>

TEST_CASE("tutorial_fast_v2") {
    uint64_t x = 1;
    ankerl::nanobench::Bench().run("++x", [&]() {
        ankerl::nanobench::doNotOptimizeAway(x += 1);
    });
}

优化不稳定

有些时候输出结果会提示你测试不稳定,你可以按照提示增加 minEpochIterations

比如:

#include <nanobench.h>
#include <doctest/doctest.h>

#include <random>

TEST_CASE("tutorial_fluctuating_v1") {
    std::random_device dev;
    std::mt19937_64 rng(dev());
    ankerl::nanobench::Bench().run("random fluctuations", [&] {
        // each run, perform a random number of rng calls
        auto iterations = rng() & UINT64_C(0xff);
        for (uint64_t i = 0; i < iterations; ++i) {
            ankerl::nanobench::doNotOptimizeAway(rng());
        }
    });
}

输出如下:

图片11

我们按照提示修改代码如下:

#include <nanobench.h>
#include <doctest/doctest.h>

#include <random>

TEST_CASE("tutorial_fluctuating_v2") {
    std::random_device dev;
    std::mt19937_64 rng(dev());
    ankerl::nanobench::Bench().minEpochIterations(5000).run(
        "random fluctuations", [&] {
            // each run, perform a random number of rng calls
            auto iterations = rng() & UINT64_C(0xff);
            for (uint64_t i = 0; i < iterations; ++i) {
                ankerl::nanobench::doNotOptimizeAway(rng());
            }
        });
}

结果果然稳定了。

比较测试结果

有时候我们需要对很多测试结果进行比较,在 nanobench 中,很容易做到,只要共用同一个 Bench 对象即可,在开始的时候调用对应的方法。

比如官方给出了一个对比不同随机数生成器的性能的例子:完整代码:example_random_number_generators.cpp

private:
    static constexpr uint64_t rotl(uint64_t x, unsigned k) noexcept {
        return (x << k) | (x >> (64U - k));
    }

    uint64_t stateA;
    uint64_t stateB;
};

namespace {

// Benchmarks how fast we can get 64bit random values from Rng.
template <typename Rng>
void bench(ankerl::nanobench::Bench* bench, char const* name) {
    std::random_device dev;
    Rng rng(dev());

    bench->run(name, [&]() {
        auto r = std::uniform_int_distribution<uint64_t>{}(rng);
        ankerl::nanobench::doNotOptimizeAway(r);
    });
}

} // namespace

TEST_CASE("example_random_number_generators") {
    // perform a few warmup calls, and since the runtime is not always stable
    // for each generator, increase the number of epochs to get more accurate
    // numbers.
    ankerl::nanobench::Bench b;
    b.title("Random Number Generators")
        .unit("uint64_t")
        .warmup(100)
        .relative(true);
    b.performanceCounters(true);

    // sets the first one as the baseline
    bench<std::default_random_engine>(&b, "std::default_random_engine");
    bench<std::mt19937>(&b, "std::mt19937");
    bench<std::mt19937_64>(&b, "std::mt19937_64");
    bench<std::ranlux24_base>(&b, "std::ranlux24_base");
    bench<std::ranlux48_base>(&b, "std::ranlux48_base");
    bench<std::ranlux24>(&b, "std::ranlux24_base");
    bench<std::ranlux48>(&b, "std::ranlux48");
    bench<std::knuth_b>(&b, "std::knuth_b");
    bench<WyRng>(&b, "WyRng");
    bench<NasamRng>(&b, "NasamRng");
    bench<Sfc4>(&b, "Sfc4");
    bench<RomuTrio>(&b, "RomuTrio");
    bench<RomuDuo>(&b, "RomuDuo");
    bench<RomuDuoJr>(&b, "RomuDuoJr");
    bench<Orbit>(&b, "Orbit");
    bench<ankerl::nanobench::Rng>(&b, "ankerl::nanobench::Rng");
}

我们需要注意的几个关键方法:

  1. unit :用于将原本默认的 xx/op 中的 op 替换为自定义的字符串。
  2. warmup :在测试开始之前进行预热的次数,也就是先执行这么些次数,不会计入最终数据。
  3. relative :设置为 true 之后,再run,之后的所有run都会以这个为基准线做对比。
  4. performanceCounters :是否测试 ins/opbra/opmiss%

上述测试结果如下:

relativens/uint64_tuint64_t/serr%ins/uint64_tbra/uint64_tmiss%totalRandom Number Generators
100.0%31.4231,828,534.725.0%219.2220.481.4%0.00std::default_random_engine
266.3%11.8084,745,762.719.3%155.6718.010.1%0.00〰️ std::mt19937 (Unstable with ~1,685.8 iters. Increase minEpochIterations to e.g. 16858)
1,019.7%3.08324,567,855.836.6%34.631.500.2%0.00〰️ std::mt19937_64 (Unstable with ~7,097.7 iters. Increase minEpochIterations to e.g. 70977)
148.9%21.1147,380,744.692.3%204.0919.000.0%0.00std::ranlux24_base
171.1%18.3654,456,268.711.4%143.5114.002.9%0.00std::ranlux48_base
47.1%66.7614,979,338.8422.8%799.1350.230.6%0.00〰️ std::ranlux24_base (Unstable with ~293.1 iters. Increase minEpochIterations to e.g. 2931)
18.2%172.825,786,199.9121.7%1,744.8785.300.3%0.00〰️ std::ranlux48 (Unstable with ~118.9 iters. Increase minEpochIterations to e.g. 1189)
64.6%48.6420,558,002.941.1%289.6020.411.2%0.00std::knuth_b
1,665.9%1.89530,227,329.500.1%10.000.0063.4%0.00WyRng
1,293.7%2.43411,770,089.697.4%23.000.00100.0%0.00〰️ NasamRng (Unstable with ~9,712.3 iters. Increase minEpochIterations to e.g. 97123)
1,197.4%2.62381,101,236.990.1%20.000.00100.0%0.00Sfc4
1,243.4%2.53395,763,921.940.1%15.000.00100.0%0.00RomuTrio
1,193.0%2.63379,720,219.701.1%14.000.00100.0%0.00RomuDuo
1,268.4%2.48403,703,084.511.0%11.000.00100.0%0.00RomuDuoJr
1,511.6%2.08481,135,323.920.7%23.002.000.0%0.00Orbit
1,309.5%2.40416,799,283.870.4%11.000.00100.0%0.00ankerl::nanobench::Rng

计算BigO

计算BigO也很简单,只需要模拟一个数据,然后将其中的 n 传入 complexityN 方法中,然后再run,它会输出对应的结果。

下面是一个测试 std::set::find BigO的代码:

#include <nanobench.h>
#include <doctest/doctest.h>

#include <iostream>
#include <set>

TEST_CASE("tutorial_complexity_set_find") {
    // Create a single benchmark instance that is used in multiple benchmark
    // runs, with different settings for complexityN.
    ankerl::nanobench::Bench bench;

    // a RNG to generate input data
    ankerl::nanobench::Rng rng;

    std::set<uint64_t> set;

    // Running the benchmark multiple times, with different number of elements
    for (auto setSize :
         {10U, 20U, 50U, 100U, 200U, 500U, 1000U, 2000U, 5000U, 10000U}) {

        // fill up the set with random data
        while (set.size() < setSize) {
            set.insert(rng());
        }

        // Run the benchmark, provide setSize as the scaling variable.
        bench.complexityN(set.size()).run("std::set find", [&] {
            ankerl::nanobench::doNotOptimizeAway(set.find(rng()));
        });
    }

    // calculate BigO complexy best fit and print the results
    std::cout << bench.complexityBigO() << std::endl;
}

上述先是模拟了一个set不断被插入,从 10 ~ 10k 的数据规模,所有数据的输入都是使用 nanobench 中提供的 Rng 类来生成随机数。

最终结果如下:

coefficienterr%complexity
3.4946962e-0946.5%O(log n)
8.0377807e-1262.9%O(n)
6.0188709e-1367.6%O(n log n)
2.6440637e-0877.4%O(1)
7.3746892e-1687.8%O(n^2)
6.7357968e-2097.0%O(n^3)

输出结果到其他格式

nanobench还支持将测试结果输出到其他文件格式,比如csv、json,或者html形成可视化界面。

还能输出到 pyperf 中进行进一步性能分析。

具体就不讲了,大家看文档:https://nanobench.ankerl.com/tutorial.html#rendering-mustache-like-templates

CLion中查看测试覆盖率

关于测试覆盖率,截一段chatgpt的对话:

图片10

其实测试覆盖率是几乎所有编译器自带的功能(CLion中暂时不支持msvc,mingw是支持的),但是需要在编译的时候加入对应的参数,但不同的编译器参数很多很繁杂,CLion就为我们提供了便利性,使用CLion你只需要点击两下鼠标就行了。

第一次点击鼠标:用于CLion帮我们生成对应的coverage配置项。

图片8

第二次点击鼠标:用于运行coverage配置项生成覆盖率数据,然后CLion将会有图形化显示。

已经生成好了对应的配置项,用对应配置项去运行这个测试即可得出结果如下:

图片9

当然你也可以自己在配置项里面添加对应的编译器flag,下面是gcc编译器的flag,别的编译器有所不同,所以CLion提供了自动帮我们生成配置项的功能。

-DCMAKE_CXX_FLAGS="--coverage"

CLion中使用sanitizers检测内存错误

关于sanitizer是什么,可以看下面这段chatgpt的截图:

图片1

现在其实在clang/gcc中已经自带了这个功能,只需要在编译时加入编译选项 -fsanitize 即可(亲测Windows下的mingw里的gcc并不支持)。

整个所有的 sanitize 功能如下:

  • AddressSanitizer (ASan):检测内存访问错误,如越界访问和使用释放的内存。它通过在程序执行期间在内存中插入虚拟填充来实现这一点,并在程序试图访问这些填充时生成错误消息。
  • LeakSanitizer (LSan):检测内存泄漏,即程序未释放的内存。它通过跟踪程序中的每个动态分配来实现这一点,并在程序结束时生成报告。
  • ThreadSanitizer (TSan):检测多线程程序中的数据竞争。它通过在程序执行期间跟踪线程之间的共享变量访问来实现这一点,并在发现竞争时生成错误消息。
  • UndefinedBehaviorSanitizer (UBSan):检测未定义行为,如类型转换错误和溢出。它通过在程序执行期间插入检查代码来实现这一点,并在发现错误时生成错误消息。
  • MemorySanitizer (MSan):检测未初始化内存的使用,这是一个非常隐蔽的错误,它通过在程序中所有未初始化内存插入值来实现这一点,并在程序试图使用这些值时生成错误消息。

其实上述的 memoryub 问题的检测,在CLion中你还未编译时就已经给出了提示。

环境准备

官方文档在这:https://www.jetbrains.com/help/clion/google-sanitizers.html,如果是windows环境可以通过安装clang-cl编译器来得到 AddressSanitizer 的能力,具体操作在这个文档中:https://www.jetbrains.com/help/clion/quick-tutorial-on-configuring-clion-on-windows.html#clang-cl

如果是 Linux/wsl/macos 环境使用 gcc/clang 都可以得到 AddressSanitizerLeakSanitizerThreadSanitizerUndefinedBehaviorSanitizer 的能力。

我推荐使用clang,至少在我的wsl上gcc的 ThreadSanitizer 能力是错误的。

如何安装clang环境就非常简单了,apt install即可。

如何使用

其实使用对应的能力很简单,只需要在编译选项中加入对应的参数即可。

set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=[sanitizer_name] [-g] [-OX]")

下面这些是 [sanitizer_name] 对应的选项:

  • address :表示开启 AddressSanitizer
  • leak : 表示开启LeakSanitizer
  • thread: 表示开启 ThreadSanitizer
  • undefined: 表示开启UndefinedBehaviorSanitizer (other options are also available, see the UBSan section)
  • memory: 表示开启MemorySanitizer

-g 是用来生成调试信息的,建议不要再选项里面加,因为CLion会根据cmake配置项里的 Release/Debug 模式来自动加上,所以你不要画蛇添足,这样会出错,加入调试信息可以让最终收集到的信息有具体的源码位置,方便我们查看分析的结果。

-ox 是优化选项,比如 -o1 -o2之类的,这个也不用管,CLion同样也是根据cmake配置项里的信息生成,比如Release就是-o3,Debug就是-o2。

设置哈对应的编译参数后,我们对需要分析的程序运行一次即可,然后CLion中就会出现图形化的结果。

内存泄漏检测(leak)

比如我现在有下面这段内存泄漏代码,我开了 -fsanitize=leak 并且为Debug模式 :

int main(){
    auto* p = new int(3234);
    (void)p;
}

最终的结果图如下:

图片2

请不要同时开启多个选项,可能会报错,如果没有报错,也可能只出现一个效果。

内存访问错误检测(address)

加入 -fsanitize=address 并设置为Debug模式。

同样我有下面这段代码(请在C++17及以上进行编译):

#include <iostream>
#include <sstream>
#include <string>
#include <string_view>
#include <vector>

struct str_helper
{
   std::string* data;
   str_helper() : data(new std::string()) {}
   ~str_helper() { delete data; }
};

std::vector<std::string_view> split_by_line_v1(std::string const& str)
{
   std::stringstream             ss(str);
   std::vector<std::string_view> ret;
   str_helper                   line;
   while (std::getline(ss, *line.data, '\n')) { ret.push_back(*line.data); }
   return ret;
}

std::vector<std::string_view> split_by_line_v2(std::string const& str)
{
   size_t pos = str.find('\n'), pre_pos = 0;
   auto   ret = std::vector<std::string_view>();
   while (pos != std::string::npos)
   {
      ret.emplace_back(str.data() + pre_pos, pos - pre_pos);
      pre_pos = pos + 1;
      pos     = str.find('\n', pre_pos);
   }
   if (pre_pos + 1 < str.size())
   {
      ret.emplace_back(str.data() + pre_pos, str.size() - pre_pos);
   }
   return ret;
}

std::vector<std::string> split_by_line_v3(std::string_view str)
{
   std::stringstream ss;
   ss << str;
   std::vector<std::string> list;
   std::string              line;
   while (std::getline(ss, line, '\n')) { list.emplace_back(std::move(line)); }
   return list;
}

int main()
{
   std::string data = "你好\n你好2\n哈哈哈哈";
   for (const auto& v : split_by_line_v1(data)) { std::cout << v << "\n"; }
}

这段代码有三个版本的split实现,很明显,第一个版本有空悬指针的问题,还有我解释为什么我第一个版本要专门再写一个 str_helper ,因为如果直接用 std::string 的话,它是检查不出来问题的,必须要使用new和delete进行内存的申请与释放才能被检测到,而标准库容器中使用的是 std::acllocator

第二个版本,没有内存安全问题,且不存在拷贝,但是使用的时候需要注意生命周期的问题,因为都是浅拷贝(string_view)。

第三个版本,没有内存安全问题,且不需要注意生命周期问题,但是有深拷贝和堆内存创建的性能损耗。

使用第一个版本检测出的情况如下图:

图片3

多线程数据竞争检测(thread)

加入 -fsanitize=thread 并设置为Debug模式。

有下列简单代码:

#include <thread>

int s_count;

void count_plus(int times)
{
   for (int i = 0; i < times; i++) ++s_count;
}

int main()
{
   std::thread th1(count_plus,100);
   std::thread th2(count_plus,100);

   th1.join();
   th2.join();
}

检测结果如下:

图片4

CLion中使用perf生成火焰图

同样截取一段chatgpt的回答:

图片11

说白了就是分析软件的性能瓶颈,具体是通过查看各个函数调用所占用的时间或cpu消耗等等。

这个功能需要下载 perf 工具,而 perf 需要Linux环境,所以Windows可以使用wsl2来实现,但是我的wsl2无法直接使用需要手动去下载wsl2内核源代码然后编译安装,安装好后,我使用后发现还是有bug(无法显示非系统调用函数),所以这个还是只适合在 Linux/macos 使用。官方文档在:https://www.jetbrains.com/help/clion/2022.3/cpu-profiler.html

环境准备

我这里就偷个懒直接把官方文档的中文翻译截图放这里了,建议自己去看官方文档:

图片7

如何使用

使用的话,只需要像下面这样配置即可:

设置图片

在CLion中运行的时候按下这个按钮即可:

图片5

运行后等一会儿,然后CLion里会有个通知告诉你可以查看profiler了,我的结果如下图(有bug,只能显示出系统调用的函数):

图片6

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/186847.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Graph】NetworkX官方基础教程:图的生成与相关操作

NetworkX官方基础教程&#xff1a;图的生成与相关操作1. Graph生成和graph操作2. 分析图3. 画图参考资料本文作为对图结构和复杂网络的快速上手&#xff0c;内容包括基于NetworkX进行图的生成与相关操作&#xff0c;Graph的分析以及绘制Graphs。 NetworkX官方基础教程 1. Grap…

Mapper代理开发

1、定义与SQL映射文件同名的Mapper接口&#xff0c;并且将Mapper接口和SQL映射文件放置在同一目录下。 在Resources下新建层级包需要用分隔符 / 2、设置SQL映射文件的namespace属性为UserMapper接口全限定名 全限定类名就是类名全称&#xff0c;带包路径的用点隔开&#xff…

Python从入门到入土的90行代码

文章目录基础入门菜鸟提升基础晋级高手之路内置包库奇技淫巧基础入门 1 python 即在命令行输入python&#xff0c;进入Python的开发环境。 2 x 12*3-4/56**2 加减乘除四则混合运算&#xff0c;可当作计算器使用&#xff0c;其中**表示乘方。 3 print(x) 即输出x的值&#x…

毕业设计:基于汇编实现的欢乐QQ堂小游戏 附完整代码

本次实现制作了汇编版的QQ堂,使用了VGA 320x200 256色视频显示, FAT12文件系统、时钟中断,nasm + gcc联合编译,通过端口设置调色板。其中除了AI,游戏界面、逻辑等均由汇编实现。游戏具有良好的图形界面,流畅的操作性,令人愉快的玩法,并且有AI与你作战。 首先展示游戏的开…

npm : 无法将“npm”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。请检查名称的拼写,如果包括路径,请确保路径 正确,然后再试一次。

npm : 无法将“npm”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。请检查名称的拼写&#xff0c;如果包括路径&#xff0c;请确保路径 正确&#xff0c;然后再试一次。解决方法方法一&#xff1a;以管理员身份运行方法二&#xff1a;查看npm环境变量配置方法一&#xf…

3.1.4 构造函数及构造代码块

文章目录1.概念2.形式3.练习4.关于构造函数的总结5.构造代码块与局部代码块5.1 形式5.2 构造代码块的特点5.3 局部代码块5.4 练习5.5 静态/构造/局部代码块的比较1.概念 构造方法是一种特殊的方法,它是一个与类同名且没有返回值类型的方法 构造方法的主要功能就是完成对象创建…

如何在android开发中使用Kotlin Flow(一)

Kotlin 的Flow可以对数据流进行建模&#xff0c;类似LiveData、RxJava的数据流。 Flow也是用观察者模式实现的。 观察者模式包括了可观察对象&#xff08;Observable&#xff0c;生产者、发射者、源这些称呼都是指可观察对象&#xff0c;可以被观察&#xff09;、观察对象&…

如何安装谷歌服务框架?(Google三件套下载教程)

谷歌服务框架&#xff0c;想必大家已经了解过了&#xff0c;都知道是个什么东西。这里我在赘述一下&#xff0c;谷歌服务框架是支持谷歌应用商店&#xff08;Google Play&#xff09;正常运行的服务框架&#xff0c;没有它&#xff0c;谷歌商店无法正常在安卓手机上运行&#x…

计算机图形学 第6章 三维变换与投影

目录 # 学习要求 前置知识 三维几何变换总的式子&#xff1a; 平移变换 比例变换 旋转变换&#xff1a;绕x轴旋转 反射变换 错切变换 三维复合变换 坐标系变换 正交投影矩阵 三视图 斜投影定义 透视投影 透视变换坐标系 ## 代码 透视投影分类 # 学习要求 …

怎么把图片做成gif动图?三步搞定gif在线制作

在日常办公、生活中经常会使用一些gif动态图片&#xff0c;生动有趣画面丰富。很好奇这些gif动图是怎么制作的吧&#xff01;其实&#xff0c;制作gif表情包的方法很简单&#xff0c;给大家分享几个简单实用的gif制作&#xff08;https://www.gif.cn/&#xff09;方法&#xff…

ccflow 代码——流程讲义

爬虫组件分析目录概述需求&#xff1a;设计思路实现思路分析1.表单引擎模版表系统框架Jflow 对使用者的要求参考资料和推荐阅读Survive by day and develop by night. talk for import biz , show your perfect code,full busy&#xff0c;skip hardness,make a better result,…

CV——day71 零基础学YOLO:YOLOv2

YOLOv25. YOLOv25.1 YOLOv2升级概述5.2 YOLOv2网络架构5.3 基于聚类提取先验框5.4 偏移量计算方法5.5 坐标映射与还原5.6 感受野的作用5.7 特征融合改进5.7.1 多尺度检测5.8 YOLOv2 总结5. YOLOv2 5.1 YOLOv2升级概述 可以看出&#xff0c;v2的map相比v1改进了很多&#xff0c…

KafkaProducer线程安全吗?

这是我今天面试被问的一个问题&#xff0c;这会回来就来看看源码 先看KafkaProducer的构造函数 public KafkaProducer(Map<String, Object> configs) { this((ProducerConfig)(new ProducerConfig(configs)), (Serializer)null, (Serializer)null); } public KafkaPro…

[MRCTF2020]PixelShooter1题解

Android一词最早出现于法国作家利尔亚当&#xff08;Auguste Villiers de lIsle-Adam&#xff09;在1886年发表的科幻小数《未来的夏娃》&#xff08;Lve future&#xff09;中。他将外表像人的机器起名为Android。 本题是一个安卓逆向 1.下载到手机玩一玩 是一个32位文件下载…

2-JVM、JRE、JDK的作用及联系和区别

在Java环境配置和项目启动中&#xff0c;这三者的配置是项目启动的基础保证。 JVM(Java Virtual Machine)&#xff0c;Java虚拟机&#xff0c;就是一个虚拟的用于解析bytecode字节码的”虚拟计算机”。一般与OS操作系统打交道。是整个java实现跨平台的最核心的部分【使用Java编…

GlobalPointer:用统一的方式处理嵌套和非嵌套NER

本文将介绍一个称为GlobalPointer的设计&#xff0c;它利用全局归一化的思路来进行命名实体识别&#xff08;NER&#xff09;&#xff0c;可以无差别地识别嵌套实体和非嵌套实体&#xff0c;在非嵌套&#xff08;Flat NER&#xff09;的情形下它能取得媲美CRF的效果&#xff0c…

大数据专业前景怎么样?

大数据专业毕业生未来的岗位选择空间比较大&#xff0c;有三大类岗位可选择分别是大数据开发岗位、大数据分析岗位和大数据运维岗位&#xff0c;在不同的行业和技术体系结构下这些岗位也包含很多细分的岗位。 大数据开发岗位分为平台研发岗位和行业场景开发岗位两大类&#xf…

latex自己记录需要的

参考:LaTeX 公式篇 推荐网站&#xff1a;LaTeX公式编辑器&#xff0c;有模板 换行 $$ \begin{array}{c} 1\\ 2\\ 3\\ \end{array} $$123\begin{array}{c} 1\\ 2\\ 3\\ \end{array} 123​ 公式拆分 $$ \begin{split} 123 & 33 \\ &6 \end{split} $$123336\begin{split}…

找工作不用愁,送你一份Salesforce面试秘籍!

在之前的文章中&#xff0c;自由侠部落为学习者梳理了Salesforce架构师的面试秘籍&#xff0c;将其拆解为了3个方面——分享工作经验、展示技术知识以及证明领导能力&#xff0c;上篇文章已经展示了工作经验部分&#xff0c;接下来将继续分享面试秘籍&#xff0c;帮助求职者顺利…

linux内核之netlink通信

Linux内核(04)之netlink通信 Author&#xff1a;Onceday Date&#xff1a;2023年1月3日 漫漫长路&#xff0c;才刚刚开始… 参考文档&#xff1a; netlink 机制 binarydady 阿里云开发者社区linux中通用Netlink详解及使用剖析 binarydady 阿里云开发者社区RFC 3549 Linux N…