【Go高性能】测试(单元测试、基准测试)
Go测试
- 一、分类
- 1. 单元测试
- 2. 基准测试
- 二、基准测试
- 1. 介绍
- 2. 基准测试基本原则
- 3. 使用testing包构建基准测试
- 3.1 执行基准测试
- 3.2 基准测试工作原理
- 3.3 改进基准测试的准确性
- 3.3.1 -benchtime
- 3.3.2 -count
- 3.3.3 -cpu
- 4. 使用benchstat工具比较基准测试(可跳过)
- 4.1 对比标准 benchmarks 和 benchstat
- 5. 避免基准测试的启动耗时
- 6. 基准测试的内存分配
- 三、单元测试
- 1. Goland自动生成
- 2. 常用断言方法
介绍如何使用 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 执行基准测试
- 方法一:直接点击方法左侧运行
- 方法二:使用
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
关注指标:
- 第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自动生成
- 把鼠标定在要测试的方法上面,右击选Generate,
- 生成测试文件
- 增加测试数据
- 运行测试
可以直接运行全部的测试方案,也可以自己选择想要运行的测试方案。
2. 常用断言方法
t.Error
和t.Fatal
:报告错误,后者还会终止测试。t.Logf
:记录日志信息。t.Errorf
:当条件不满足时,记录错误并继续执行后续测试。