Go语言学习18-基准测试

引言

所谓基准测试(Benchmark Test,简称BMT)是指,通过一些科学的手段实现对一类测试对象的某项性能指标进行可测量、可重复和可比对的测试。很多时候,基准测试已被狭义地称为性能测试。

主要内容

1. 编写基准测试函数

与功能测试相同,针对其他源码文件中的程序实体的基准测试程序也是以测试函数为单位的。一个基准测试函数的名称和签名如下:

1
func BenchmarkXxx(b *testing.B)

2. 计时器

testing.B 类型中,与计时器相关的方法有3个它们是 StartTimerStopTimer* 和 ResetTimer 。这3个方法被用于操纵基准测试函数的计时器。该计时器的作用是计算当前基准测试函数的执行时间。

调用 b.StartTimer 方法意味着开始对当前的测试函数的执行进行计时。它总会在开始执行基准测试函数的时候被自动地调用。这个方法被暴露出来的意义在于:计时器在被停止之后重新启动。调用 b.StopTimer 方法可以使当前测试函数的计时器停止。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package bmt

import (
"testing"
"time"
)

func Benchmark(b *testing.B) {
customTimerTag := false
if customTimerTag {
b.StopTimer()
}
b.SetBytes(12345678)
time.Sleep(time.Second)
if customTimerTag {
b.StartTimer()
}
}

如上文件命名为 bmt_test.go 存放到工作区的 testing/bmt 代码包中,运行基准测试截图如下:

现在将其中的 customTimerTag 变量的值改为 true ,再来运行测试,截图如下:

从上面两个运行截图可以看见最后两行的输出内容不同。在第二个截图中,倒数第二行的3个部分分别代表了当前测试函数的名称,操作次数以及操作平均耗时。其中,操作次数 是当前的基准测试函数被执行的次数,而 操作平均耗时 是当前基准测试函数的平均执行时间。

同样观察两个运行截图的倒数第二行可知,当 customTimerTagtrue 时,基准测试函数 Benchmark 可以被执行多次,而当 customTimerTagfalse 时,它往往只能获得一次执行机会。这些都是由于 testing 包中有这样的一个限制:在基准测试函数单次执行时间超过指定值(默认为1秒,也可以由标记 -benchtime 自定义)的情况下,只执行该基准测试函数一次。也就是测试运行程序会在不超过这个执行时间上限的情况下尽可能多次地执行一个基准测试函数。

customTimerTagtrue 时,在调用语句 time.Sleep(time.Second) 的之前和之后,分别停止和重启了 Benchmark 函数的计时器,这就相当于不把 time.Sleep(time.Second) 语句的执行时间算在 Benchmark 函数的执行时间之内,执行 Benchmark 函数的时间已经基本可以忽略不计了(可以从 0.00 ns/op 可知),这样测试运行程序在 Benchmark 函数的累积执行时间为到达时间上限之前就会连续不断地重复执行它。

customTimerTagfalse 时,调用语句 time.Sleep(time.Second) 让当前的测试程序“休息”1 秒,Benchmark 函数的单次执行时间就肯定会大于 1 秒。因此测试运行程序就不会对 Benchmark 函数执行第二次。

对于方法 b.ResetTimer 在被调用时,会重置当前基准测试函数的计时器,就是把该函数的执行时间重置为 0,这相当于把当前函数中在 b.ResetTimer 语句之前的所有语句的执行时间都从该函数的执行时间中减去。

3. 内存分配统计

方法 b.ReportAllocs 的含义是判断在启动当前测试的 go test 命令的后面是否有 -benchmem 标记。它会返回一个 bool 类型的结果值。

方法 b.SetBytes 接受一个 int64 类型的值,它被用于记录在单次操作中被处理的字节的数量。

customTimerTagfalse,运行截图中,针对 Benchmark 函数的操作信息的那一行信息中多处了一个部分—— 12.34MB/s 。它的含义是每秒被处理的字节的数量(以 MB 为单位)。这个数量其实等于测试运行程序在执行(可能是多次) Benchmark 函数的过程中每秒调用 b.SetBytes 方法的次数乘以传入的那个整数。

首先试想一个场景:在基准测试函数 Benchmark 中测试的是一个向文件系统中写入数据的函数。在写入成功后,会调用 b.SetBytes 方法并把真正写入的字节数作为参数传入。通过测试结果信息中的 xxx MB/s ,可以获知该函数每秒能向文件系统写入多少兆字节( MB )的数据了。

从上面总结,b.SetBytes 方法能够从输入输出(IO)的角度统计出被测试的程序实体的实际性能。

4. 基准测试的运行

在上面的测试中,go test 命令只运行了 cnet/ctcp 包中的功能测试。下面说说 go test 命令的基准测试标记说明。

标记名称 标记描述
-bench regexp 在默认情况下,go test命令不会运行任何基准测试,但可以使用该标记以执行匹配“regexp”处的
正则表达式的基准测试函数,“regexp”可以被替换成任何正则表达式。如果需要运行所有的基准测试函数,
添加 –bench . 或 –bench=. 或 –bench=“.”
-benchmem 在输出内容中包含基准测试的内存分配统计信息
-benchtime t 用来间接地控制单个基准测试函数的操作次数。这里的“t”指的是执行单个测试函数的累积耗时上限。
“t”处的内容使用的是类型time.Duration可接受的时间表示法。“t”的默认值是1s

运行针对代码包 cnet/ctcp 运行基准测试的截图如下:

结构体类型 testing.B 的字段 N 可以被用来设置对基准测试函数中的某一个代码块的重复执行次数。例如:

1
2
3
for i := 0; I < b.N; i++ {
//测试代码
}

运行截图如下:

对于计算 N 的值的具体算法,可以查看标准库的 testing 包的源码文件 benchmark.go 中的相关代码。

如果要看到基准测试函数的操作次数和操作平均耗时的同时获得这个过程中的内存分配情况,就需要用到 -benchmem 标记。例如下面截图:

23416 B/op”是每次操作分配的字节的平均数为23416个。“109 allocs/op”是每次操作分配内存的次数平均为109次。

go test命令还可以接受一个可自定义测试运行次数并在测试运行期间改变Go语言最大并发处理数的标记 -cpu , -cpu 标记可以是一个整数列表,多个整数之间用逗号分隔。**-cpu** 标记的处理方式和 -parallel 标记相反,**-parallel** 标记默认使用Go语言最大并发处理数,而 -cpu 标记却会直接设置它。但是,由 -cpu 标记引发的Go语言最大并发处理数的设置操作并不会影响 -parallel 标记的默认值。因为 -parallel 标记的值是在测试运行程序初始化的时候设置的。如果在 go test 命令中没有显式地加入 -parallel 标记,则它的值会被设置为测试运行程序初始化时刻的Go语言最大并发处理数。在这个时刻,测试程序运行还没有把 -cpu 标记的值(如果有的话)解析成整数数组,也就无法使用这个数组中的整数设置Go语言最大并发处理数了。

使用 -cpu 标记运行截图如下:

测试运行程序执行基准测试函数 BenchmarkPrimeFuncs 的次数是 7,这与 -cpu 标记的值 1,2,4,8,12,16,20 中的 7 个数字相对应。上面只是展示了基准测试的运行记录,同样这边也调用了 7 次功能测试函数 TestPrimeFuncs

如上运行截图中,倒数 行的末尾包含了一行运行时环境信息:**[GOMAXPROCS=20, NUM_CPU=4, NUM_GOROUTINE=2],对于第一个 GOMAXPROCS 代表Go语言最大并发处理数,此处为 20 , NUM_CPU 代表当前计算机的CPU总内核数,此处为 4NUM_GOROUTINE** 代表当前时刻的并发程序的数量,此处为 2

与并发处理有关的标记

标记名称 使用示例 说明
-parallel -parallel 4 功能:设置可并发执行的功能测试函数的最大数量
默认值:调用runtime.GOMAXPROCS(0)后的结果,即Go语言最大并发处理数量
先决条件:功能测试函数需要在开始处调用结构体testing.T类型的参数值的Parallel方法
生效的测试:功能测试
-cpu -cpu 1,2,4 功能:根据标记的值,迭代的设置Go语言并发处理最大数并执行全部功能测试或全部基准测试。
迭代的次数与标记值中的整数个数一致
默认值:“”,即空字符串
先决条件:无
生效的测试:功能测试和基准测试

注意: -cpu-parallel 标记的作用域都是代码包,它们只能用于控制某一个代码包内的测试的流程。如果使用 go test 命令启动了多个代码包的测试,那么每个代码包中的功能测试永远是可并发执行的,而基准测试永远是串行执行的。如果把针对某一个代码包的所有测试的运行过程看成一个整体的话,若在执行 go test 命令时加入了 -bench 标记,则针对各个代码包的测试运行过程会被串行地执行,否则它们将被并发地执行。但无论如何,打印测试记录和结果信息的动作是严格按照 go test 命令后面的代码包从左往右的顺序执行。

结语

本篇介绍了Go语言的基准测试的相关内容,下一篇讲解Go语言的样本测试,敬请期待!!!