9.3 基准测试
基准测试是一种测试代码性能的方法。想要测试解决同一问题的不同方案的性能,以及查看哪种解决方案的性能更好时,基准测试就会很有用。基准测试也可以用来识别某段代码的 CPU 或者内存效率问题,而这段代码的效率可能会严重影响整个应用程序的性能。许多开发人员会用基准测试来测试不同的并发模式,或者用基准测试来辅助配置工作池的数量,以保证能最大化系统的吞吐量。
让我们看一组基准测试的函数,找出将整数值转为字符串的最快方法。在标准库里,有 3 种方法可以将一个整数值转为字符串。
代码清单 9-28 展示了 listing28_test.go 基准测试开始的几行代码。
代码清单 9-28 listing28_test.go:第 01 行到第 10 行
01 // 用来检测要将整数值转为字符串,使用哪个函数会更好的基准
02 // 测试示例。先使用 fmt.Sprintf 函数,然后使用
03 // strconv.FormatInt 函数,最后使用 strconv.Itoa
04 package listing28_test
05
06 import (
07 "fmt"
08 "strconv"
09 "testing"
10 )
和单元测试文件一样,基准测试的文件名也必须以 _test.go
结尾。同时也必须导入 testing
包。接下来,让我们看一下其中一个基准测试函数,如代码清单 9-29 所示。
代码清单 9-29 listing28_test.go:第 12 行到第 22 行
12 // BenchmarkSprintf 对 fmt.Sprintf 函数
13 // 进行基准测试
14 func BenchmarkSprintf(b *testing.B) {
15 number := 10
16
17 b.ResetTimer()
18
19 for i := 0; i < b.N; i++ {
20 fmt.Sprintf("%d", number)
21 }
22 }
在代码清单 9-29 的第 14 行,可以看到第一个基准测试函数,名为 BenchmarkSprintf
。基准测试函数必须以 Benchmark
开头,接受一个指向 testing.B
类型的指针作为唯一参数。为了让基准测试框架能准确测试性能,它必须在一段时间内反复运行这段代码,所以这里使用了 for
循环,如代码清单 9-30 所示。
代码清单 9-30 listing28_test.go:第 19 行到第 22 行
19 for i := 0; i < b.N; i++ {
20 fmt.Sprintf("%d", number)
21 }
22 }
代码清单 9-30 第 19 行的 for
循环展示了如何使用 b.N
的值。在第 20 行,调用了 fmt
包里的 Sprintf
函数。这个函数是将要测试的将整数值转为字符串的函数。
基准测试框架默认会在持续 1 秒的时间内,反复调用需要测试的函数。测试框架每次调用测试函数时,都会增加 b.N
的值。第一次调用时, b.N
的值为 1
。需要注意,一定要将所有要进行基准测试的代码都放到循环里,并且循环要使用 b.N
的值。否则,测试的结果是不可靠的。
如果我们只希望运行基准测试函数,需要加入 -bench
选项,如代码清单 9-31 所示。
代码清单 9-31 运行基准测试
go test -v -run="none" -bench="BenchmarkSprintf"
在这次 go test
调用里,我们给 -run
选项传递了字符串 "none"
,来保证在运行制订的基准测试函数之前没有单元测试会被运行。这两个选项都可以接受正则表达式,来决定需要运行哪些测试。由于例子里没有单元测试函数的名字中有 none
,所以使用 none
可以排除所有的单元测试。发出这个命令后,得到图 9-14 所示的输出。
图 9-14 运行单个基准测试
这个输出一开始明确了没有单元测试被运行,之后开始运行 BenchmarkSprintf
基准测试。在输出 PASS
之后,可以看到运行这个基准测试函数的结果。第一个数字 5000000
表示在循环中的代码被执行的次数。在这个例子里,一共执行了 500 万次。之后的数字表示代码的性能,单位为每次操作消耗的纳秒(ns)数。这个数字展示了这次测试,使用 Sprintf
函数平均每次花费了 258 纳秒。
最后,运行基准测试输出了 ok
,表明基准测试正常结束。之后显示的是被执行的代码文件的名字。最后,输出运行基准测试总共消耗的时间。默认情况下,基准测试的最小运行时间是 1 秒。你会看到这个测试框架持续运行了大约 1.5 秒。如果想让运行时间更长,可以使用另一个名为 -benchtime
的选项来更改测试执行的最短时间。让我们再次运行这个测试,这次持续执行 3 秒(见图 9-15)。
图 9-15 使用-benchtime 选项来运行基准测试
这次 Sprintf
函数运行了 2000 万次,持续了 5.384 秒。这个函数的执行性能并没有太大的变化,这次的性能是每次操作消耗 256 纳秒。有时候,增加基准测试的时间,会得到更加精确的性能结果。对大多数测试来说,超过 3 秒的基准测试并不会改变测试的精确度。只是每次基准测试的结果会稍有不同。
让我们看另外两个基准测试函数,并一起运行这 3 个基准测试,看看哪种将整数值转换为字符串的方法最快,如代码清单 9-32 所示。
代码清单 9-32 listing28_test.go:第 24 行到第 46 行
24 // BenchmarkFormat 对 strconv.FormatInt 函数
25 // 进行基准测试
26 func BenchmarkFormat(b *testing.B) {
27 number := int64(10)
28
29 b.ResetTimer()
30
31 for i := 0; i < b.N; i++ {
32 strconv.FormatInt(number, 10)
33 }
34 }
35
36 // BenchmarkItoa 对 strconv.Itoa 函数
37 // 进行基准测试
38 func BenchmarkItoa(b *testing.B) {
39 number := 10
40
41 b.ResetTimer()
42
43 for i := 0; i < b.N; i++ {
44 strconv.Itoa(number)
45 }
46 }
代码清单 9-32 展示了另外两个基准测试函数。函数 BenchmarkFormat
测试了 strconv
包里的 FormatInt
函数,而函数 BenchmarkItoa
测试了同样来自 strconv
包的 Itoa
函数。这两个基准测试函数的模式和 BenchmarkSprintf
函数的模式很类似。函数内部的 for
循环使用 b.N
来控制每次调用时迭代的次数。
我们之前一直没有提到这 3 个基准测试里面调用 b.ResetTimer
的作用。在代码开始执行循环之前需要进行初始化时,这个方法用来重置计时器,保证测试代码执行前的初始化代码,不会干扰计时器的结果。为了保证得到的测试结果尽量精确,需要使用这个函数来跳过初始化代码的执行时间。
让这 3 个函数至少运行 3 秒后,我们得到图 9-16 所示的结果。
图 9-16 运行所有 3 个基准测试
这个结果展示了 BenchmarkFormat
测试函数运行的速度最快,每次操作耗时 45.9 纳秒。紧随其后的是 BenchmarkItoa
,每次操作耗时 49.4 ns。这两个函数的性能都比 Sprintf
函数快得多。
运行基准测试时,另一个很有用的选项是 -benchmem
选项。这个选项可以提供每次操作分配内存的次数,以及总共分配内存的字节数。让我们看一下如何使用这个选项(见图 9-17)。
图 9-17 使用-benchmem 选项来运行基准测试
这次输出的结果会多出两组新的数值:一组数值的单位是 B/op
,另一组的单位是 allocs/op
。单位为 allocs/op
的值表示每次操作从堆上分配内存的次数。你可以看到 Sprintf
函数每次操作都会从堆上分配两个值,而另外两个函数每次操作只会分配一个值。单位为 B/op
的值表示每次操作分配的字节数。你可以看到 Sprintf
函数两次分配总共消耗了 16 字节的内存,而另外两个函数每次操作只会分配 2 字节的内存。
在运行单元测试和基准测试时,还有很多选项可以用。建议读者查看一遍所有选项,以便在编写自己的包和工程时,充分利用测试框架。社区希望包的作者在正式发布包的时候提供足够的测试。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论