0%

Go语言实战学习笔记-go基准测试

Go语言实战学习笔记-go基准测试

最近看完Go语言实战 这本书,对其中的内容记录下学习笔记

什么是基准测试

基准测试是一种测试代码性能的方法。想要测试解决同一问题的不同方案的性能,以及查看
哪种解决方案的性能更好时,基准测试就会很有用。基准测试也可以用来识别某段代码的CPU
或者内存效率问题,而这段代码的效率可能会严重影响整个应用程序的性能。许多开发人员会用
基准测试来测试不同的并发模式,或者用基准测试来辅助配置工作池的数量,以保证能最大化系
统的吞吐量

示例代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// 用来检测要将整数值转为字符串,使用哪个函数会更好的基准
// 测试示例。先使用fmt.Sprintf 函数,然后使用
// strconv.FormatInt 函数,最后使用strconv.Itoa
package listing05_test

import (
"fmt"
"strconv"
"testing"
)

// BenchmarkSprintf 对fmt.Sprintf 函数
// 进行基准测试
func BenchmarkSprintf(b *testing.B) {
number := 10

b.ResetTimer()

for i := 0; i < b.N; i++ {
fmt.Sprintf("%d", number)
}
}

// BenchmarkFormat 对strconv.FormatInt 函数
// 进行基准测试
func BenchmarkFormat(b *testing.B) {
number := int64(10)

b.ResetTimer()

for i := 0; i < b.N; i++ {
strconv.FormatInt(number, 10)
}
}

// BenchmarkItoa 对strconv.Itoa 函数
// 进行基准测试
func BenchmarkItoa(b *testing.B) {
number := 10

b.ResetTimer()

for i := 0; i < b.N; i++ {
strconv.Itoa(number)
}
}

代码讲解

基准测试的文件名也必须以_test.go 结尾。同时也必须导入 testing 包。接下来,让我们看一下其中一个基准测试函数
在代码的第39 行,可以看到第一个基准测试函数,名为BenchmarkSprintf。
基准测试函数必须以Benchmark 开头,接受一个指向testing.B 类型的指针作为唯一参数。
为了让基准测试框架能准确测试性能,它必须在一段时间内反复运行这段代码,所以这里使用了
for 循环展示了如何使用b.N 的值。在第20 行,调用了fmt 包里的Sprintf 函数。这个函数是将要测试的将整数值转为字符串的函数。

基准测试框架默认会在持续1 秒的时间内,反复调用需要测试的函数。测试框架每次调用测试函数时,都会增加b.N 的值。第一次调用时,b.N 的值为1。需要注意,一定要将所有要进行基准测试的代码都放到循环里,并且循环要使用b.N 的值。否则,测试的结果是不可靠的。
如果我们只希望运行基准测试函数,需要加入-bench 选项
命令如下:

1
go test -v -run="none" -bench="BenchmarkSprintf"

在这次 go test 调用里,我们给-run 选项传递了字符串”none”,来保证在运行制订的基
准测试函数之前没有单元测试会被运行。这两个选项都可以接受正则表达式,来决定需要运行哪
些测试。由于例子里没有单元测试函数的名字中有none,所以使用none 可以排除所有的单元
测试。发出这个命令后,得到如下所示的输出。

结果

这个输出一开始明确了没有单元测试被运行,之后开始运行BenchmarkSprintf 基准测试。
在输出PASS 之后,可以看到运行这个基准测试函数的结果。第一个数字4978968 表示在循环中的代码被执行的次数。在这个例子里,一共执行了497 万次。之后的数字表示代码的性能,
单位为每次操作消耗的纳秒(ns)数。这个数字展示了这次测试,使用Sprintf 函数平均每次花费了253.9 纳秒

最后,运行基准测试输出了ok,表明基准测试正常结束。之后显示的是被执行的代码文件的名字。
最后,输出运行基准测试总共消耗的时间。默认情况下,基准测试的最小运行时间是1 秒。你会看到
这个测试框架持续运行了大约1.5 秒。如果想让运行时间更长,可以使用另一个名为-benchtime 的
选项来更改测试执行的最短时间。让我们再次运行这个测试,这次持续执行3 秒(如下图):
结果

这次 Sprintf 函数运行了1100 万次,持续了4.130 秒。这个函数的执行性能并没有太大的
变化,这次的性能是每次操作消耗292.7 纳秒。有时候,增加基准测试的时间,会得到更加精确的性能结果。对大多数测试来说,超过3 秒的基准测试并不会改变测试的精确度。只是每次基准测试的结果会稍有不同

基准测试里面调用b.ResetTimer 的作用。在代码开始执行循环之前需要进行初始化时,这个方法用来重置计时器,保证测试代码执行前的初始化代码,不会干扰计时器的结果。为了保证得到的测试结果尽量精确,需要使用这个函数来跳过初始化代码的执行时间。

让这 3 个函数至少运行3 秒后,我们得到
结果

这个结果展示了BenchmarkFormat 测试函数运行的速度最快,每次操作耗时11.09 纳秒。紧随其
后的是BenchmarkItoa,每次操作耗时11.25 ns。这两个函数的性能都比Sprintf 函数快得多。
运行基准测试时,另一个很有用的选项是-benchmem 选项。这个选项可以提供每次操作分
配内存的次数,以及总共分配内存的字节数。让我们看一下如何使用这个选项
结果

这次输出的结果会多出两组新的数值:一组数值的单位是B/op,另一组的单位是
allocs/op。单位为allocs/op 的值表示每次操作从堆上分配内存的次数