Go微服务实战
上QQ阅读APP看书,第一时间看更新

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函数时参数一直在变化,执行所需的时间变化幅度比较大,无法进入稳态,这种情况下基准测试函数不会给出测试结果。因此必须保证基准测试函数的执行结果收敛于一个数值,这样才可以得到测试结果。