浅谈c++性能测试工具google benchmark

一、测试对象

这次测试的对象是标准库的vector,我们将会在vs2019 16.10和Linux + GCC 11.1上进行测试。为了代码写着方便,我还会启用c++17支持。

这次的疑问来自于《A Tour of C++》这本书,最近在重新翻阅本书的时候发现书里第九章给出了一个很有意思的建议:尽量少用reserve方法。

我们都知道reserve会提前分配足够大的内存来容纳元素,这样在push_back时可以减少内存分配和元素移动的次数,从而提高性能。所以习惯上如果我们知道vector中存储元素的大致数量,就会使用reserve提前进行内存分配,这是典型的“空间换时间”。

而书中给出的理由仅仅是说vector的内存分配器性能已经很高,预分配往往是多此一举,收效甚微。事实到底如何呢,性能问题光靠脑补是不能定位的,所以我们用实验结果说话。

二、使用模板函数生成测试

测试用例的设计很简单,我们定义普通vector和reserve过的vector,然后分别对其添加一定数量的元素(逐步从少到多)测试性能。

同时vector本身是泛型容器,所以为了测试的全面性我们需要测试两至三种类型参数。

如果针对每一种情况写测试函数,显然违反了DRY原则,因为除了vector的类型参数不同,其他代码几乎是完全一样的。

对于上面的需求,就需要模板测试函数登场了:

template <typename T, std::size_t length, bool is_reserve = true>
void bench_vector_reserve(benchmark::State& state)
{
	for (auto _ : state) {
		std::vector<T> container;
		if constexpr (is_reserve) {
			container.reserve(length);
		}
		for (std::size_t i = 0; i < length; ++i) {
			container.push_back(T{});
		}
	}
}

非常的简单,我们通过length控制插入的元素个数;is_reserve则负责控制是否预分配内存,通过if constexpr可以生成reserve和不进行任何操作的两种代码。

然后我们像往常一样定义一个测试用例:

BENCHMARK(bench_vector_reserve<std::string,100>);

可是等我们编译的时候却报错了!

$ g++ test.cpp -lpthread -lbenchmark -lbenchmark_main

test.cpp:19:48: 错误:宏“BENCHMARK”传递了 2 个参数,但只需要 1 个

   19 | BENCHMARK(bench_vector_reserve<std::string,100>);

      |                                                ^

In file included from a.cpp:1:

/usr/local/include/benchmark/benchmark.h:1146: 附注:macro "BENCHMARK" defined here

 1146 | #define BENCHMARK(n)                                     \

      | 

test.cpp:19:1: 错误:‘BENCHMARK'不是一个类型名

   19 | BENCHMARK(bench_vector_reserve<std::string,100>);

原因是这样的,在编译器处理宏的时候实际上不会考虑c++语法,所以分割模板参数的逗号被识别成了分割宏参数的逗号,因此在宏处理器的眼里我们像是传了两个参数。这也说明了BENCHMARK是处理不了模板的。

不过别担心,Google早就想到这种情况了,所以提供了BENCHMARK_TEMPLATE宏,我们只需要把模板名字和需要的类型参数依次传给宏即可:

BENCHMARK_TEMPLATE(bench_vector_reserve, std::string, 100);
BENCHMARK_TEMPLATE(bench_vector_reserve, std::string, 1000);
BENCHMARK_TEMPLATE(bench_vector_reserve, std::string, 10000);
BENCHMARK_TEMPLATE(bench_vector_reserve, std::string, 100000);
BENCHMARK_TEMPLATE(bench_vector_reserve, std::string, 100, false);
BENCHMARK_TEMPLATE(bench_vector_reserve, std::string, 1000, false);
BENCHMARK_TEMPLATE(bench_vector_reserve, std::string, 10000, false);
BENCHMARK_TEMPLATE(bench_vector_reserve, std::string, 100000, false);

现在就可以正常编译运行了:

可以看到reserve过的容器性能几乎比默认的快了一倍。

不过在揭晓为什么书上不推荐reserve的谜底之前,我们的代码还有可以简化的地方。

三、定制测试参数

首当其冲的问题其实还是违反了DRY原则——除了数字,其他内容都是重复的。

看到这种代码直觉就告诉我该做些改进了。

Ranges接受start和end两个int64_t类型的参数,默认从start起每次累乘8,一直达到end。

通过RangeMultiplier我们可以改变乘数,比如从8改成10。

在这里我们的length参数其实是不必要的,所以代码可以这样改:

template <typename T, bool is_reserve = true>
void bench_vector_reserve(benchmark::State& state)
{
	for (auto _ : state) {
		std::vector<T> container;
		if constexpr (is_reserve) {
            // 通过range方法获取传入的参数
			container.reserve(state.range(0));
		}
		for (std::size_t i = 0; i < state.range(0); ++i) {
			container.push_back(T{});
		}
	}
}

BENCHMARK_TEMPLATE(bench_vector_reserve, std::string)->RangeMultiplier(10)->Range(10, 10000 * 10);
BENCHMARK_TEMPLATE(bench_vector_reserve, std::string, false)->RangeMultiplier(10)->Range(10, 10000 * 10);

现在我们测试的元素数量是[10, 100, 1000, 10^4, 10^5]

除此之外还有另一种叫“密集参数”的Ranges。google benchmark提供了DenseRange方法。

这个方法的原型如下:

DenseRange(int64_t start, int64_t end, int64_t step);

Ranges是累乘,而DenseRange是累加,因为累乘会导致几何级数的增长,在数轴上的分布越来越稀疏,累加则看上去像是均匀分布的,因此累加的参数生成器被叫做密集参数生成器。

如果我们把测试用例这么改:

BENCHMARK_TEMPLATE(bench_vector_reserve, std::string)->DenseRange(1000, 100 * 100, 1000);

现在我们的length就是这样一个序列:[1000,2000,3000, ...,9000,10000]

关于自定义参数最后一个知识点是ArgsProduct。看名字就知道这是一个参数工厂。

ArgsProduct(const std::vector< std::vector<int64_t> >& arglists);

std::vector<int64_t>实际上就是一组参数,arglists就是多组参数的合集,他们之间会被求笛卡尔积,举个例子:

BENCHMARK(BM_test)->ArgsProduct({ {"a", "b", "c", "d"}, {1, 2, 3, 4} });

// 等价于下面的
BENCHMARK(BM_test)->Args({"a", 1})
                  ->Args({"a", 2})
                  ->Args({"a", 3})
                  ->Args({"a", 4})
                  ->Args({"b", 1})
                  ->Args({"b", 2})
                  ->Args({"b", 3})
                  ...
                  ->Args({"d", 3})
                  ->Args({"d", 4})

我们可以看到参数工厂其实得自己手写所有参数,那如果我想配合工厂使用Ranges呢?

没问题,benchmark的开发者们早就想到了,所以提供了下面这些帮助函数:

benchmark::CreateRange(8, 128, /*multi=*/2)   // 生成:[8, 16, 32, 64, 128]
benchmark::CreateDenseRange(1, 6, /*step=*/1) // 生成:[1, 2, 3, 4, 5, 6]

如果换成我们的例子,就可以这样写:

BENCHMARK_TEMPLATE(bench_vector_reserve, std::string)->ArgsProduct({
    benchmark::CreateRange(10, 10000*10, 10)
});
BENCHMARK_TEMPLATE(bench_vector_reserve, std::string, false)->ArgsProduct({
    benchmark::CreateRange(10, 10000*10, 10)
});

借助仅仅两行代码我们就能生成数量可观的测试用例:

当然,这只是一个类型参数,实际上我们还有另外两个类型需要测试。另外这是1.5.5新增的功能,如果你想尝鲜得先升级google benchmark。

四、进一步简化

通常做到上面那一步就足够了,然而在这里我们还有优化空间,因为如果我们把其他两个测试用的类型加上,代码是这样的,MyClass的定义后面会给出:

BENCHMARK_TEMPLATE(bench_vector_reserve, std::string)->ArgsProduct({
    benchmark::CreateRange(10, 10000*10, 10)
});
BENCHMARK_TEMPLATE(bench_vector_reserve, std::string, false)->ArgsProduct({
    benchmark::CreateRange(10, 10000*10, 10)
});
BENCHMARK_TEMPLATE(bench_vector_reserve, std::size_t)->ArgsProduct({
    benchmark::CreateRange(10, 10000*10, 10)
});
BENCHMARK_TEMPLATE(bench_vector_reserve, std::size_t, false)->ArgsProduct({
    benchmark::CreateRange(10, 10000*10, 10)
});
BENCHMARK_TEMPLATE(bench_vector_reserve, MyClass)->ArgsProduct({
    benchmark::CreateRange(10, 10000*10, 10)
});
BENCHMARK_TEMPLATE(bench_vector_reserve, MyClass, false)->ArgsProduct({
    benchmark::CreateRange(10, 10000*10, 10)
});

你看见了什么?没错,重复重复重复!我们又违背了DRY原则。

重复说不上什么十恶不赦,但能避免还是要避免的,所以我准备用宏来简化这些代码:

#define generate_test(type) \
	BENCHMARK_TEMPLATE(bench_vector_reserve, type)->ArgsProduct({benchmark::CreateRange(10, 100000, 10)}); \
	BENCHMARK_TEMPLATE(bench_vector_reserve, type, false)->ArgsProduct({benchmark::CreateRange(10, 100000, 10)});

generate_test(std::string);
generate_test(std::size_t);
generate_test(MyClass);

这下舒服多了。

接着来看我们的MyClass,我们的MyClass包含几个虚函数,禁止移动赋值,同时被刻意设计成了非平凡复制,这样的类型可以说是绕过了标准库容器设计的大部分优化措施,算是个妥妥的反面教材,希望你的项目里尽量不要出现这种东西:

class MyClass {
public:
	long i = 2L;
    MyClass() { i = 2L; }
	virtual ~MyClass() {}
	virtual long get() { return i; }
	MyClass& operator=(MyClass&&) = delete;
	MyClass(const MyClass& obj) {
		i = obj.i;
	}
	MyClass& operator=(const MyClass& obj) {
		i = obj.i;
	}
};

这个类其实就是针对内存分配器实现的,vector在重新进行内存分配后很可能靠移动语义或者memmove来移动数据,这两者将导致重新分配内存导致的性能损失变小,不利于我们观察vector的行为,所以我定制了这个类。

这是Windows上的结果,Linux上也差不多,到目前为止我们看到reserve过的vector有着惊人的性能,那书里说的到底是怎么回事呢?

五、揭晓答案

实际上上面测试的都是我们明确知道vector一定会被插入N个元素不多不少的情况。

然而这种情况其实在开发中是不多见的,更多的时候我们只能得到vector里元素数量的平均数、众数,甚至是一个没什么可靠依据的经验值。

所以试想一下这种情况,reserve给的参数是1000,而我的vector总是会插入1001~1005个参数,显然1000是不够的,除了reserve外还会进行一次内存分配,而且这次分配后很可能还需要把原先的元素都转移过去(realloc不是总能找到合适的位置扩展已有内存,而且像MyClass那样的类在c++17中是不能bitwise复制的),那么这样的开销究竟如何呢?我们还是拿测试说话。

篇幅有限,所以我只能简单模拟一下上述情况:

template <typename T, bool is_reserve = true>
void bench_vector_reserve(benchmark::State& state)
{
	for (auto _ : state) {
		std::vector<T> container;
		if constexpr (is_reserve) {
			container.reserve(state.range(0));
		}
        // 每次多插入两个元素,这样多半会导致一次内存分配(当然不保证一定会)
		for (std::size_t i = 0; i < state.range(0)+2; ++i) {
			container.push_back(T{});
		}
	}
}

编译均使用Release模式和默认的优化级别,这是Linux上的测试结果:

和我们预期的一样,多出来的一次内存分配使reserve带来的性能优势荡然无存。

有意思的是Windows上的结果:

奇怪的事情发生了,虽说多出的一次分配缩小了性能差距,但reserve任然带来了明显的优势。

这里我就不卖关子了,我们直接看vector的源码。

首先是GCC11.1的,代码在/usr/include/c++/11.1.0/bits目录下,分散在vector.tccstl_vector.h中,其中push_back在容器内存不够的时候会用_M_realloc_insert重新分配足够的内存,这个函数在vector.tcc的432行有定义,使用_M_check_len计算重新分配的内存大小。

_M_check_len是关键,定义在stl_vector.h的1756行:

// Called by _M_fill_insert, _M_insert_aux etc.
size_type
_M_check_len(size_type __n, const char* __s) const
{
	if (max_size() - size() < __n)
	  __throw_length_error(__N(__s));

	const size_type __len = size() + (std::max)(size(), __n);
	return (__len < size() || __len > max_size()) ? max_size() : __len;
}

__n在push_back的时候是1,所以不难看出GCC的vector的扩容策略是每次扩容一倍。

vs2019的stl实现开源在了github。关键代码在这里,push_back在内存不够的时候会调用_Emplace_reallocate,里面会调用_Calculate_growth计算重新分配的内存大小:

_CONSTEXPR20_CONTAINER size_type _Calculate_growth(const size_type _Newsize) const {
    // given _Oldcapacity and _Newsize, calculate geometric growth
    const size_type _Oldcapacity = capacity();
    const auto _Max              = max_size();

    if (_Oldcapacity > _Max - _Oldcapacity / 2) {
        return _Max; // geometric growth would overflow
    }

    const size_type _Geometric = _Oldcapacity + _Oldcapacity / 2; // 关键代码

    if (_Geometric < _Newsize) {
        return _Newsize; // geometric growth would be insufficient
    }

    return _Geometric; // geometric growth is sufficient
}

_Newsize相当于前面GCC的__n,在push_back的时候是1,所以不难看出vs2019的vector增长策略是每次扩容0.5倍。

除此之外两者的剩余部分大同小异,都是先分配新内存,然后在新内存里构建要插入的元素,再把其他元素移动到新内存里,就连移动元素的方式也差不多,都是先尝试memmove,接着试试移动语义,最后让复制操作兜底。

那么两者肉眼可见的区别就只有扩容策略这一条了。所以这会带来什么影响呢?看个例子:

#include <iostream>
#include <vector>

void test1(std::size_t len)
{
	std::vector<int> v1, v2;
	v2.reserve(len);
	for (std::size_t i = 0; i < len; ++i) {
		v1.push_back(1);
		v2.push_back(1);
	}
	std::cout << "v1: " << v1.capacity() << '\n';
	std::cout << "v2: " << v2.capacity() << '\n';
}

void test2(std::size_t len)
{
	std::vector<int> v1, v2;
	v2.reserve(len);
	for (std::size_t i = 0; i < len + 1; ++i) {
		v1.push_back(1);
		v2.push_back(1);
	}
	std::cout << "v1: " << v1.capacity() << '\n';
	std::cout << "v2: " << v2.capacity() << '\n';
}

int main()
{
	test1(100000);
	test2(100000);
}

/*
vs2019的运行结果:
v1: 138255
v2: 100000
v1: 138255
v2: 150000

GCC11.1.0的结果:
v1: 131072
v2: 100000
v1: 131072
v2: 200000
*/

如果是一个有10万个元素的vector想要扩容,GCC就会比vs多分配50000个元素需要的内存,分配如此多的内存需要花费更多的时间,即使reserve带来了性能优势在这一步也都挥霍的差不多了。

激进的扩容策略让GCC出现了明显的性能波动,不过这只是出现上面那样测试结果的原因之一,两个标准库的allocator实现上的区别也可能是其中一个原因。不过msvc的分配器实现并不公开,所以最终是什么导致了上述的结果并不能轻易断言。

六、总结

我们学习了如何使用模板和参数生成器创建大量测试用例,以提高编写测试的效率。

我们还顺便了解了vector.reserve对性能的影响,总结规律有几条:

1.如果明确知道vector要存放元素的具体数量,推荐reserve,性能提升是有目共睹的;

2.否则你不应该使用reserve,一来有提前优化之嫌,二是在使用libstdc++的程序上会产生较大的性能波动;

3.接上一条,reserve使用不当还会造成内存的浪费。

看来《A Tour of C++》的作者只说对了一半,这也证明了性能问题不能靠臆断,一定要靠测试来定位、解决。

以上就是浅谈c++性能测试工具google benchmark的详细内容,更多关于c++性能测试工具google benchmark的资料请关注我们其它相关文章!

(0)

相关推荐

  • 详解C++中StringBuilder类的实现及其性能优化

    介绍 经常出现客户端打电话抱怨说:你们的程序慢如蜗牛.你开始检查可能的疑点:文件IO,数据库访问速度,甚至查看web服务. 但是这些可能的疑点都很正常,一点问题都没有. 你使用最顺手的性能分析工具分析,发现瓶颈在于一个小函数,这个函数的作用是将一个长的字符串链表写到一文件中. 你对这个函数做了如下优化:将所有的小字符串连接成一个长的字符串,执行一次文件写入操作,避免成千上万次的小字符串写文件操作. 这个优化只做对了一半. 你先测试大字符串写文件的速度,发现快如闪电.然后你再测试所有字符串拼接的速

  • C++性能剖析教程之循环展开

    什么是循环展开? 循环展开,英文中称Loop unwinding或loop unrolling,是一种牺牲程序的尺寸来加快程序的执行速度的优化方法.可以由程序员完成,也可由编译器自动优化完成.循环展开最常用来降低循环开销,为具有多个功能单元的处理器提供指令级并行.也有利于指令流水线的调度. 循环展开能从两方面改进程序的性能: 减少了不直接有助于程序结果的操作的数量,例如循环索引计算和分支条件. 提供了一些方法,可以进一步变化代码,减少整个计算中关键路径上的操作数量. 循环展开对程序性能的影响 我

  • C++实现高性能转换大小写算法示例

    简述 最近工作中遇到一个需求,是需要将URL中的 query 参数的key全部转换为小写或者大写,键值对的数量有点多,但全部都是英文字母,无需考虑非字母的情况. 实现比较快的做法是使用STL或C标准库中的转换接口,如下: #include <string> #include <cctype> #include <algorithm> // 字符串中的大写字符转小写 std::string strtolower(std::string s) { transform(s.b

  • 总结c++性能优化策略

    1 关于继承:不可否认良好的抽象设计可以让程序更清晰,代码更看起来更好,但是她也是有损失的,在继承体系中子类的创建会调用父类的构造函数,销毁时会调用父类的析构函数,这种消耗会随着继承的深度直线上升,所以不要过度的抽象和继承. 2 对象的复合:对象的复合和继承很相似,当一个对象包含其他对象构造时也会引起额外的构造.关于这点可能会有很多人不解,认为这是不可避免的,举个例子,你的一个对象中用到数组和字符串,你是选择string和vector还是char* 和c系的数组呢,如果没有用到c++stl库提供

  • 浅谈C++性能榨汁机之伪共享

    前言 在多核并发编程中,如果将互斥锁的争用比作"性能杀手"的话,那么伪共享则相当于"性能刺客"."杀手"与"刺客"的区别在于杀手是可见的,遇到杀手时我们可以选择战斗.逃跑.绕路.求饶等多种手段去应付,但"刺客"却不同,"刺客"永远隐藏在暗处,伺机给你致命一击,防不胜防.具体到我们的并发编程中,遇到锁争用影响并发性能情况时,我们可以采取多种措施(如缩短临界区,原子操作等等)去提高程序性能,

  • 浅谈c++性能测试工具之计算时间复杂度

    google benchmark已经为我们提供了类似的功能,而且使用相当简单. 具体的解释在后面,我们先来看几个例子,我们人为制造几个时间复杂度分别为O(n), O(logn), O(n^n)的测试用例: // 这里都是为了演示而写成的代码,没有什么实际意义 static void bench_N(benchmark::State& state) { int n = 0; for ([[maybe_unused]] auto _ : state) { for (int i = 0; i <

  • C++性能剖析教程之switch语句

    前言 几乎每本面向初学者的C语言或C++书籍在前面两章都会提到分支控制语句if--else和switch--case,在某些情况下这两种分支控制语句可以互相替换,但却很少有人去深究在if--else和switch--case语句的背后到底有什么异同?应该选择哪一个语句才能使得效率最高?要回答这些问题,只能走到switch语句的背后,看看这些语句到底是怎么实现的. 基本格式 switch语句的基本格式如下: switch (表达式) { case 常量表达式1:<语句序列1><break;

  • C++编写高性能服务器实例教程

    我将展示如何使用现代C++编写一个Echo服务器,相当于分布式系统开发中的"Hello World".这个服务器会将接收的消息直接返回.我们同时需要一个可以向我们的服务器发动消息的客户端,在这里可以发现客户端的源码. Wangle是一个用来搭建事件驱动的现代异步C++服务的C/S应用框架.Wangle最基本的抽象概念就是Pipeline(管线).能够理解这种抽象,将会很容易写出各种复杂的现代C++服务,另一个重要的概念是Service(服务),其可以看作一种更高级的Pipeline,不

  • 浅谈c++性能测试工具google benchmark

    一.测试对象 这次测试的对象是标准库的vector,我们将会在vs2019 16.10和Linux + GCC 11.1上进行测试.为了代码写着方便,我还会启用c++17支持. 这次的疑问来自于<A Tour of C++>这本书,最近在重新翻阅本书的时候发现书里第九章给出了一个很有意思的建议:尽量少用reserve方法. 我们都知道reserve会提前分配足够大的内存来容纳元素,这样在push_back时可以减少内存分配和元素移动的次数,从而提高性能.所以习惯上如果我们知道vector中存储

  • 浅谈webpack构建工具配置和常用插件总结

    webpack构建工具已经火了好几年,也是当下狠火狠方便的构建工具,我们没有理由不去学习.既然选择webpack就要跟着时代走,我们要追随大牛的步伐,大牛等等我. 一.webpack基础 在根目录使用npm init 命令创建package.json,创建过程中一路回车. 本地安装webpack命令:npm install webpack webpack-cli --save-dev 安装成功后写入package.js的devDependencies中,可以通过 npm node_modules

  • 浅谈JDK14性能管理工具之jmap和jhat

    简介 jmap(Java Memory Map)是JDK自带的工具,用来将某个java程序的内存中的信息打印或者输出到文件中,然后通过jhat(Java Heap Analysis Tool)工具对输出的文件进行分析,从而找到可能出现的问题. 接下来进入我们的jmap和jhat之旅吧. jmap jmap -clstats <pid>     to connect to running process and print class loader statistics jmap -finali

  • 浅谈Xcode 开发工具 XCActionBar

    XCActionBar 是一个用于 Xcoded 的通用生产工具. 下载地址:https://github.com/pdcgomes/XCActionBar 基本命令: (1)「command+shift+8」或者双击「command」键可以打开「动作输入框窗口」 (2)「command+option+7」或者双击「alt」键可以执行「上次的动作」 编程时可用于双击或三击事件的按键分别为如下5个: (1)「alt」:NSAlternateKeyMask (2)「command」:NSComman

  • 浅谈常用字符串与集合类转换的工具类

    在项目中,我们经常需要把接收到的字符串转换成对应的集合类保存,或者把集合类转换成字符串以方便传输,这个工具类中封装了几个常用的方法,对于这种转换需求十分方便. import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Properties; import java.u

  • 浅谈python中的数字类型与处理工具

    python中的数字类型工具 python中为更高级的工作提供很多高级数字编程支持和对象,其中数字类型的完整工具包括: 1.整数与浮点型, 2.复数, 3.固定精度十进制数, 4.有理分数, 5.集合, 6.布尔类型 7.无穷的整数精度 8.各种数字内置函数及模块. 基本数字类型 python中提供了两种基本类型:整数(正整数金额负整数)和浮点数(注:带有小数部分的数字),其中python中我们可以使用多种进制的整数.并且整数可以用有无穷精度. 整数的表现形式以十进制数字字符串写法出现,浮点数带

  • 浅谈Android Studio 3.0 工具新特性的使用 Android Profiler 、Device File Explorer

    前言: 其实 studio3.0的工具大家也已经使用过一段时间了,自己呢,就是从bate版开始使用的,我觉得比较好用的几个地方.就几个,可能还没用到其他的精髓. 但我觉的这个两个功能对我是比较实用的.好那么下面就给大家介绍一下吧. 正文: 话不多说咱们直接上图吧.(个人比较喜欢看图说话) 第一个(Android Profiler)我要介绍的就是这个了.(先看一下效果"震撼一下") (图-1) (图-2) (图-3) (厉害不厉害,牛逼不牛逼)那么我们怎么来操作这个工具呢,来咱们接着看图

  • 浅谈python实现Google翻译PDF,解决换行的问题

    我们复制PDF到Google翻译时,总是会出现换行的情况,如果自己手动去除,那就太麻烦了. 那么用Python就可以解决,复制到粘贴板以后,Python程序自动可以把\n换成空格,然后我们就可以复制到Google翻译中去 代码: import pyperclip import time import webbrowser copyBuff=' ' while True: time.sleep(10) copyedText=pyperclip.paste() if copyBuff!=copyed

  • 浅谈java运用注解实现对类中的方法检测的工具

    创建自定义注解 @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Test { } 建立测试类 public class UserTest { @Test public void testInsert() { User user = null; System.out.println(user.getUsername()); } @Test public void testQuery(

随机推荐