6.3.2 基准测试函数
基准测试就是性能测试,上一节介绍过,runtime/pprof包可以进行性能分析,此外Go语言还提供了基准测试功能。虽然两者都可以进行性能分析,但是Go官方提供的基准测试性能更优,而且使用go test也可以生成数据文档。
在使用runtime/pprof包的时候,需要在代码中先引入这个包,如果使用Benchmark函数则可以单独写测试函数,保证代码的整洁性。基准测试函数是以Benchmark开头的函数,也是放在以_test.go结尾的文件内的。
仍以斐波那契数列为例,因为斐波那契数列的计算会使用函数的递归调用功能,系统有较大的系统开销。本节将会通过几种不同的方式来完成斐波那契数列的计算,然后分析各种方式的性能差异。
现在来看一下具体示例代码:
book/ch06/6.3/bhmark/testBenchmark.go
1. package main
2.
3. import "fmt"
4.
5. func fb1(n int) int {
6. if n == 0 {
7. return 0
8. }else if n == 1 {
9. return 1
10. }else {
11. return fb1(n-1) + fb1(n-2)
12. }
13.
14. }
15.
16. func fb2(n int) int {
17. if n == 0 || n == 1 {
18. return n
19. }
20. return fb2(n-1) + fb2(n-2)
21. }
22.
23. func fb3(n int) int {
24. fbMap := make(map[int]int)
25. for i := 0;i <= n;i++ {
26. var t int
27. if i <= 1 {
28. t = i
29. }else {
30. t = fbMap[i-1] + fbMap[i-2]
31. }
32. fbMap[i] = t
33. }
34. return fbMap[n]
35. }
36.
37. func main() {
38. fmt.Println(fb1(50))
39. fmt.Println(fb2(50))
40. fmt.Println(fb3(50))
41. }
42. //以下是执行结果
43. 12586269025
44. 12586269025
45. 12586269025
这里写了三个函数,都是用于计算斐波那契数列的。
fb1函数是较为一般的实现,与6.3.1节写的斐波那契函数基本一致。fb2函数是对fb1函数进行微小优化后得到的,即把原来的if else语句改为了if的逻辑或判断,后续可以在基准测试部分查看这两个函数是否在性能上有明显差异。fb3函数的改变最大,没有再使用函数的递归调用功能,而是通过map来存储整个数列,最后返回第n个对应的值,这是典型的以空间换时间的方法。不过fb3的效率是不是最高,还要通过Benchmark函数来测试。在main函数中分别传入用三个函数计算n=50的返回值,从最后的打印结果来看,三个函数得到的结果是一样的。可以说,在功能测试方面,还没有找到明显bug。下面直接进行Benchmark函数的测试。
下面来完成测试代码,在同一个路径下创建testBeanchmark_test.go文件,内容如下:
book/ch06/6.3/bhmark/testBenchmark_test.go
1. package main
2.
3. import "testing"
4.
5. var final int
6. func benchmarkfb1(b *testing.B,n int) {
7. var end int
8. for i := 0; i < b.N; i++ {
9. end = fb1(n)
10. }
11. final = end
12. }
13.
14. func Benchmarkfb2(b *testing.B,n int) {
15. var end int
16. for i := 0; i < b.N; i++ {
17. end = fb2(n)
18. }
19. final = end
20. }
21.
22. func Benchmarkfb3(b *testing.B,n int) {
23. var end int
24. for i := 0; i < b.N; i++ {
25. end = fb3(n)
26. }
27. final = end
28. }
29.
30. func Benchmark50fb1(b *testing.B) {
31. benchmarkfb1(b,50)
32. }
33.
34. func Benchmark50fb2(b *testing.B) {
35. Benchmarkfb2(b,50)
36. }
37.
38. func Benchmark50fb3(b *testing.B) {
39. Benchmarkfb3(b,50)
40. }
第6行至第28行分别为前面的fb1、fb2和fb3这三个函数写了三个基准测试函数。注意每个函数都有参数testing.B。testing.B拥有testing.T的全部接口,也可以判定测试是成功还是失败,并给出PASS或FAIL的报告信息,同时可以记录日志信息。testing.B与testing.T最显著的特征是新增了整型成员N,用来指定被测试函数的执行次数,这主要是为了找出稳定状态下被测函数的性能。所以,在调用被测函数的时候,三个函数都执行了0到N次的循环,最终统计结果会打印执行测试及平均每次循环消耗的时间。
同时,要注意第6行的函数名称,b是小写的。这种写法是让包外不可见,也就是在执行go test的时候不会自动执行这个函数。而其实即便是函数名称大写,如第14行和第22行那样,也是在包外不可见的,因为虽然是以Benchmark开头的,但是其参数却不仅是testing.B,还多了一个int类型,所以go test不会把该函数认定为基准测试函数。真正的基准测试函数是像第30行至第40行的三个函数,它们不仅以Benchmark开头,参数也只有testing.B。
Benchmark函数与Test函数一样,在Benchmark后面必须跟大写字母或数字,本例当中是跟数字,用来说明要测试的是求第几个斐波那契数。
接下来,还要使用go test来执行基准测试:
$ go test -bench=. goos: darwin goarch: amd64 Benchmark50fb1-4 1 74412117830 ns/op Benchmark50fb2-4 1 81948932462 ns/op Benchmark50fb3-4 300000 5026 ns/op PASS ok command-line-arguments 157.930s
最终可以看到,fb1和fb2的基准测试都只被执行了一次,且耗时都很长。也正是因为耗时太长,所以程序的执行次数设置的是1 ,这时可以调整参数50为30,再次执行,结果如下:
goos: darwin goarch: amd64 Benchmark50fb1-4 300 4828324 ns/op Benchmark50fb2-4 300 5046923 ns/op Benchmark50fb3-4 500000 3172 ns/op PASS ok command-line-arguments 5.598s
注意,这里直接修改了代码,没有重新写Benchmark代码。这时fb1和fb2各执行了300次,但fb2还是比fb1的平均耗时高,而fb3的效率最高。显然,执行次数多的情况下得出的平均耗时更有说服力。
输出信息前两行是操作系统和处理器架构,也就是环境变量GOOS和GOARCH的值。输出信息最后一行是总的执行时间。
基准测试函数在程序进入稳态后返回数据,如果程序每次执行的时间一直不停地变化,就无法进入稳态,基准测试函数就不会有任何返回数据,比如:
func BenchmarkFb1(b *testing.B) { for i:=0;i<b.N;i++{ _ = fb1(i) } }
对于上面的使用方式,虽然在函数的命名规则和参数的使用上都符合基准测试的要求,但是却不会有任何返回,因为每次调用fb1函数时参数一直在变化,执行所需的时间变化幅度比较大,无法进入稳态,这种情况下基准测试函数不会给出测试结果。因此必须保证基准测试函数的执行结果收敛于一个数值,这样才可以得到测试结果。