Go语言SDK内置了很多实用的工具,默认自带了单元测试、基准测试命令;我们先看看基准测试如何使用。

什么是基准测试

百度百科:基准测试是指通过设计科学的测试方法、测试工具和测试系统,实现对一类测试对象的某项性能指标进行定量的和可对比的测试。例如,对计算机CPU进行浮点运算、数据访问的带宽和延迟等指标的基准测试,可以使用户清楚地了解每一款CPU的运算性能及作业吞吐能力是否满足应用程序的要求。可测量、可重复、可对比是基准测试的三大原则,其中可测量是指测试的输入和输出之间是可达的,也就是测试过程是可以实现的,并且测试的结果可以量化表现;可重复是指按照测试过程实现的结果是相同的或处于可接受的置信区间之内,而不受测试的时间、地点和执行者的影响;可对比是指一类测试对象的测试结果具有线性关系,测试结果的大小直接决定性能的高低。

基准测试就是用来测试应用程序性能的,基准测试完成之后,更重要的是结果比对;通过比对帮我们选型做决策依据,或者帮助我们改进应用程序性能。相当优秀的程序员,必须掌握基准测试的使用。

如何做基准测试

先看一个简单的测试示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// fib_test.go
func fib(n int) int {
	if n == 0 || n == 1 {
		return n
	}
	return fib(n-2) + fib(n-1)
}

func Benchmark_fib(b *testing.B) {
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		fib(10)
	}
}

上面的代码放入fib-test.go文件中,如何运行基准测试呢?很简单,在控制台中定位到当前文件所在的目录,运行下面的代码:

go test -bench=. -run=none

会看到下面的结果

Benchmark_fib    4286938               274 ns/op
PASS
ok      gofast/fst/test 1.601s

解释一下这个命令。go test 命令默认只做单元测试,如果想做基准测试必须加上 -bench 参数,加了这个参数单元测试和基准测试都会做。如果只做基准测试不做单元测试就加上 -run=xxx 这样的参数,来告诉工具只做名称为TESTxxx的单元测试,而一般没有这样名字的单元测试函数,所以这等同于所有的单元测试都不做了。

一般我们都是对整个目录进行测试,命令最后可以加上一个当前目录 .或者干脆就不写也成。参数还有很多,我们找几个常用的说一下。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
-v 				# 打印详细日志

-bench=.		# 运行所有的基准测试
-bench=_fib		# 只运行Benchmark_fib
-bench=fib$		# 匹配所有fib结尾的
-benchtime=3s	# 每一轮运行3秒
-benchtime=300x	# 循环运行300次
-cpu=2,4		# 指定GOMAXPROCS的数量,模拟多核。分别2核和4核运行一次测试
-count=3		# 运行3轮

-benchmem		# 显示堆内存分配情况,分配的越多越影响性能

总结下基准测试函数编写的一些要点:

  1. 基准测试的代码文件必须以_test.go结尾,废话,单元测试不也这样吗
  2. 基准测试的函数必须以Benchmark开头,必须是可导出的
  3. 基准测试函数必须接受一个指向Benchmark类型的指针作为唯一参数
  4. 基准测试函数不能有返回值
  5. b.ResetTimer是重置计时器,这样可以避免for循环之前的初始化代码的耗时干扰
  6. 被测试的代码要放在for循环中,b.N是测试框架提供的,表示循环的次数,他会反复调用测试的代码,最后评估性能

再看两个名词解释(这两个值越小越好,证明内存利用的非常高效):

  • allocs/op 表示每个操作(单次迭代)发生了多少个不同的内存分配。
  • B/op每个操作分配了多少字节。

基准测试对比

做基准测试的目的就是让我们做更好的决策。很多时候我们都需要创造一致的测试环境,测试不同代码的性能差异做对比,据此选出更优秀的方案。下面看一个性能比对的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func BenchmarkSprintf(b *testing.B) {
	num := 10
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		_ = fmt.Sprintf("%d", num)
	}
}

func BenchmarkFormat(b *testing.B) {
	num := int64(10)
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		strconv.FormatInt(num, 10)
	}
}

func BenchmarkItoa(b *testing.B) {
	num := 10
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		strconv.Itoa(num)
	}
}

运行结果go test -bench=. -run=none -benchmem -benchtime=3s

BenchmarkSprintf        58911222                 62.7 ns/op            2 B/op          1 allocs/op
BenchmarkFormat         1000000000               2.15 ns/op            0 B/op          0 allocs/op
BenchmarkItoa           1000000000               2.16 ns/op            0 B/op          0 allocs/op
PASS
ok      gofast/fst/test 12.439s

看到性能差异了吧?运行结果很清楚的告诉我们,Sprintf的性能只能是后两种的一半,原因很大程度上值每次都发生了堆内存的分配。

StopTimer 和 StartTimer

这两个函数需要注意下,他们组合能实现高级功能。

每次函数调用前后需要一些准备和清理工作,我们可以用 StopTimer 暂停计时器,再使用 StartTimer 开始计时。(操作系统内核时间只能精确到3毫秒,用这种方法能起作用吗?表示怀疑)

例如,如果测试一个冒泡函数的性能,每次调用冒泡函数前,需要随机生成一个数字序列,这是非常耗时的操作,这种场景下,就需要使用 StopTimerStartTimer 避免将这部分时间计算在内。

SetBytes显示数据处理带宽

通过SetBytes添加每次处理的数据大小,可以统计数据处理带宽,示例:

func BenchmarkXXX(b *testing.B) {
	b.SetBytes(int64(len(data)))
	b.ReportAllocs()
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		XXX()
	}
}

并发测试

上面的基准测试都是单线程非并发的测试结果,其实还有一种模拟并发的基准测试:

func BenchmarkLoop(b *testing.B) {
	b.ReportAllocs()
	b.ResetTimer()

	// 设置并发数
	b.SetParallelism(5000)
	// 测试多线程并发模式
	b.RunParallel(func(pb *testing.PB) {
		for pb.Next() {
			loopModelFunc()
		}
	})
}

RunParallel并发的执行benchmark。RunParallel创建多个goroutine然后把b.N个迭代测试分布到这些goroutine上。goroutine的数目默认是GOMAXPROCS。如果要增加non-CPU-bound的benchmark的并个数,在执行RunParallel之前调用SetParallelism。

这个并发模式的测试更符合功能模块的真实场景。

参考:

https://zhuanlan.zhihu.com/p/80578541

(完)