【Go高性能】测试(单元测试、基准测试)


介绍如何使用 Go 语言的标准库 testing 进行测试。

参考:①https://cloud.tencent.com/developer/article/2211864
②https://geektutu.com/post/quick-go-test.html#7-Benchmark-%E5%9F%BA%E5%87%86%E6%B5%8B%E8%AF%95

一、分类

在Go语言的世界里,testing包是进行单元测试和基准测试的核心组件。它不仅简化了测试流程,还通过简洁明了的API鼓励开发者编写高质量的测试代码。

Go 语言推荐测试文件和源代码文件放在一块,测试文件以 _test.go 结尾。

1. 单元测试

  • 功能: 测试单个函数/方法,报告测试失败、记录日志、设置测试状态等。
  • 示例: 在 func TestXxx(t *testing.T) 中使用,适用于验证代码的正确性。

2. 基准测试

  • 功能:用于基准测试(性能测试),记录执行时间和内存分配等性能指标。
  • 示例: 在 func BenchmarkXxx(b *testing.B) 中使用,适用于测量代码的性能。

二、基准测试

1. 介绍

要想改进程序的性能,首先要知道程序的当前性能。
benchmark 是 go 语言中用于测试基准性能的工具。该工具用于测试被测试函数的平均运行耗时、内存分配次数。主要适用于在已知性能瓶颈在哪里时的场景。通过对相同功能函数的不同实现的性能指标(平均运行耗时、平均内存分配次数)进行比较,以判断性能的优劣。

2. 基准测试基本原则

为了保证基准测试结果的相对稳定性,需要保持硬件环境的稳定。即:

  • 机器处于空闲状态
  • 机器关闭了节能模式
  • 避免使用虚拟机和云主机

不过如果单纯测试某个函数的性能,精准性要求也不高的话,没必要这么细致啦~

3. 使用testing包构建基准测试

Go的tesing包中内置了基准测试功能。在编写基准测试时基本和编写单元测试的原则相似:

  • 文件名必须以 _test.go 为后缀
  • 函数名必须以 BenchmarkXxxx开头
  • 基准测试函数的参数类型是 *Testing.B,而非 *Testing.T

需要测试函数:

func singleFunc() {
	time.Sleep(100 * time.Millisecond)
}

基准测试代码:

func Benchmark_singleFunc(b *testing.B) {
	for n := 0; n < b.N; n++ {
		singleFunc()
	}
}

3.1 执行基准测试

  1. 方法一:直接点击方法左侧运行
    在这里插入图片描述
  2. 方法二:使用 go test -bench=要测的方法名 ./方法所在包的相对路径
    例如:go test -bench=singleFunc(已在函数所在包的路径下)
  • -bench 标记使用正则表达式来匹配要运行的基准测试函数名称。所以,最常用的方式是通过-bench . 标记来执行该包下的所有的基准函数。
  • 默认情况下,执行go test命令时 只会执行单元测试,而基准测试会被排除在外。所以,需要在 go test 命令中添加 -bench 标记,以执行基准测试。

基准测试报告每一列值对应的含义如下:

type BenchmarkResult struct {
    N         int           // 迭代次数
    T         time.Duration // 基准测试花费的时间
    Bytes     int64         // 一次迭代处理的字节数
    MemAllocs uint64        // 总的分配内存的次数
    MemBytes  uint64        // 总的分配内存的字节数
}

测试结果:

1. goos: windows
2. goarch: amd64
3. pkg: method/f
4. Benchmark_singleFunc
5. Benchmark_singleFunc-6                10         101990650 ns/op
5. PASS

关注指标:

  1. 第5行Benchmark_singleFunc-6 10 101990650 ns/op
  • Benchmark_singleFunc: 这是基准测试的名称,表示测试的函数名。
  • -6: 这是运行基准测试的 goroutine 数量(并发数)。和运行该测试用例时的GOMAXPROCS值有关系,默认为启动时 Go 进程可见的 CPU 数。在这个例子中,6表示使用了 6 个 goroutine 来并行执行基准测试。
  • 10:是基准测试在测试期间执行的总次数(b.N 的值)。
  • 101990650 ns/op:是每次操作的平均耗时,单位是纳秒(ns)。

3.2 基准测试工作原理

每个基准函数被执行时都有一个b.N值,该值是由go运行时自动生成的, 代表基准函数应该执行的次数。

b.N 从 1 开始,基准函数默认要运行 1 秒,如果该函数的执行时间在 1 秒内就运行完了,那么就递增 b.N 的值,再重新再执行一次。

3.3 改进基准测试的准确性

3.3.1 -benchtime

基准测试运行的时间越长,迭代次数越多,最终的平均值结果越准确。

如果你的基准测试只执行了 100 次或 10 次迭代,那么最终得出的平均值可能会偏高。如果你的基准测试执行了上百万或十亿次迭代,那么得出的平均耗时将会非常准确。

可以使用 -benchtime 标识指定基准测试执行的时间以调整迭代次数(即b.N的值),以便得到更准确的结果。例如:

PS E:\Program Data\GoProject\basic\method\f> go test -bench=singleFunc -benchtime=10s
goos: windows
goarch: amd64
pkg: method/f
Benchmark_singleFunc-6               100         103430246 ns/op
PASS
ok      method/f        13.644s

执行以上命令,直到其达到 b.N 的值需要花费超过 10 秒的时间才能返回。由于我们的运行时间增加了 10 倍,因此迭代的总次数也增加了 10 倍。结果(每次操作耗时 103430246ns/op) 没有太大的变化,说明我们的数据相对比较稳定,是我们所期望的。
如果你有一个基准测试运行了数百万次或数十亿次迭代,你可能会发现基准值不稳定,因为你的机器硬件的散热性能、内存局部性、后台进程、gc 等因素都会影响函数执行的时间。

3.3.2 -count

通过 -count 标志,可以指定基准测试跑多次,以消除上述的不稳定因素:

 go test -bench . -benchtime 2s -count 5 
goos: windows
goarch: amd64
pkg: method/f
Benchmark_singleFunc-6                22         103056855 ns/op
Benchmark_singleFunc-6                21         102299486 ns/op
Benchmark_singleFunc-6                22         104436250 ns/op
Benchmark_singleFunc-6                21         102005167 ns/op
Benchmark_singleFunc-6                21         102501433 ns/op
PASS
ok      method/f        14.764s
3.3.3 -cpu
go test -bench . -cpu 1,2,4            
goos: windows
goarch: amd64
pkg: method/f
Benchmark_singleFunc                  10         102037000 ns/op
Benchmark_singleFunc-2                10         101757150 ns/op
Benchmark_singleFunc-4                10         102318930 ns/op
PASS
ok      method/f        6.607s

该示例展示了分别用 CPU 为 1 核、2 核、4 核时运行基准测试的结果。

4. 使用benchstat工具比较基准测试(可跳过)

由于基准测试受电源管理、后台进程、散热的影响,所以对于任何一个基准测试来说,运行多次来求平均值是一个非常好的建议。

下面介绍一个由 Russ Cox 编写的工具:benchstat

% go get golang.org/x/perf/cmd/benchstat

benchstat 可以对一组基准测试的结果求平均值,并显示出对应的稳定性。这是函数在使用电池的电脑上执行的基准示例:

% go test -bench . -benchtime 2s -count 5 | tee old.txt
goos: windows
goarch: amd64
pkg: method/f
Benchmark_singleFunc-6                22         103056855 ns/op
Benchmark_singleFunc-6                21         102299486 ns/op
Benchmark_singleFunc-6                22         104436250 ns/op
Benchmark_singleFunc-6                21         102005167 ns/op
Benchmark_singleFunc-6                21         102501433 ns/op
PASS
ok      method/f        14.764s

% benchstat old.txt
name     time/op
singleFunc-6  102.5ms ± 2%

平均操作耗时是102.5毫秒,并且误差在 +/-2%。

4.1 对比标准 benchmarks 和 benchstat

确定两组基准测试结果之间的差异可能是单调乏味且容易出错的。 Benchstat 可以帮助我们解决这个问题。

提示 : 保存基准运行的输出很有用,但你也可以保存生成它的二进制文件。 为此,请使用-c标志来保存测试二进制文件;我经常将这个二进制文件从.test重命名为.golden。

% go test -c
% mv fib.test fib.golden 

为了比较新版本,我们编译了一个新的测试二进制文件并对它们都进行了基准测试,并使用benchstat对输出进行比较。

% go test -c
% ./fib.golden -test.bench=. -test.count=10 > old.txt
% ./fib.test -test.bench=. -test.count=10 > new.txt
% benchstat old.txt new.txt
name     old time/op  new time/op  delta
Fib20-8  44.3µs ± 6%  25.6µs ± 2%  -42.31%  (p=0.000 n=10+10)

比较基准测试时需要检查三件事

  • 新老两次的方差。1-2% 是不错的, 3-5% 也还行,但是大于5%的话,可能不太可靠。 在比较一方具有高差异的基准时要小心,您可能看不到改进。
  • p值。p值低于0.05是比较好的情况,大于0.05则意味着基准测试结果可能没有统计学意义。
  • 样本不足。benchstat将报告它认为有效的新旧样本的数量,有时你可能只发现9个报告,即使你设置了-count=10。拒绝率小于10%一般是没问题的,而高于10%可能表明你的设置是不稳定的,也可能是比较的样本太少了。

5. 避免基准测试的启动耗时

有时候基准测试每次执行的时候会有一次启动配置耗时。b.ResetTimer() 函数可以用于忽略启动的累积耗时。如下

func BenchmarkExpensive(b *testing.B) {
    boringAndExpensiveSetup() //启动配置。默认这里的执行时间是被计算在内的
    b.ResetTimer()
    for n := 0; n < b.N; n++ {
        //function under test
    }
}

在上例代码中,使用 b.ResetTimer() 函数重置了基准测试的计时器

如果在每次循环迭代中,你有一些费时的配置逻辑,要使用 b.StopTimer()b.StartTimer() 函数来暂定基准测试计时器。

func BenchmarkComplicated(b *testing.B) {
    for n := 0; n < b.N;n++ {
        b.StopTimer()
        complicatedSetup()
        b.StartTimer()
        //function under test
    }
}

6. 基准测试的内存分配

内存分配的次数和分配的大小跟基准测试的执行时间相关。在基准测试中有两种方式可以记录并输出内存分配:

  • 在代码中增加 b.ReportAllocs() 函数来告诉 testing 框架记录内存分配的数据。
  • 在go test命令中添加 -benchmem 标识来强制 testing 框架打印出所有基准测试的内存分配次数

方式一:代码中添加 b.ReportAllocs()

func BenchmarkRead(b *testing.B) {
    b.ReportAllocs()
    for n := 0; n < b.N; n++ {
        //function under test
    }
}

方式二:go test命令中添加 -benchmem标识

%  go test -run=^$ -bench=. -benchmem bufio
goos: darwin
goarch: amd64
pkg: bufio
BenchmarkReaderCopyOptimal-8            13860543                82.8 ns/op            16 B/op          1 allocs/op
BenchmarkReaderCopyUnoptimal-8           8511162               137 ns/op              32 B/op          2 allocs/op
BenchmarkReaderCopyNoWriteTo-8            379041              2850 ns/op           32800 B/op          3 allocs/op
BenchmarkReaderWriteToOptimal-8          4013404               280 ns/op              16 B/op          1 allocs/op
BenchmarkWriterCopyOptimal-8            14132904                82.7 ns/op            16 B/op          1 allocs/op
BenchmarkWriterCopyUnoptimal-8          10487898               113 ns/op              32 B/op          2 allocs/op
BenchmarkWriterCopyNoReadFrom-8           362676              2816 ns/op           32800 B/op          3 allocs/op
BenchmarkReaderEmpty-8                   1857391               639 ns/op            4224 B/op          3 allocs/op
BenchmarkWriterEmpty-8                   2041264               577 ns/op            4096 B/op          1 allocs/op
BenchmarkWriterFlush-8                  87643513                12.5 ns/op             0 B/op          0 allocs/op
PASS
ok      bufio   13.430s

第四列是每次操作的平均内存分配大小,单位是字节(B)。
第五列是每次操作的平均内存分配次数(allocations)。

三、单元测试

单元测试通常放置在与被测试文件同目录下的_test.go文件中。测试函数必须以Test开头,后接被测试函数名,接受一个t *testing.T参数。

1. Goland自动生成

  1. 把鼠标定在要测试的方法上面,右击选Generate,
    在这里插入图片描述
    在这里插入图片描述
  2. 生成测试文件
    在这里插入图片描述
  3. 增加测试数据
    在这里插入图片描述
  4. 运行测试
    可以直接运行全部的测试方案,也可以自己选择想要运行的测试方案。

2. 常用断言方法

  • t.Errort.Fatal:报告错误,后者还会终止测试。
  • t.Logf:记录日志信息。
  • t.Errorf:当条件不满足时,记录错误并继续执行后续测试。
  • 18
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值