目录

Go 基准测试

Go 语言标准库内置的 testing 测试框架提供了基准测试(benchmark)的能力,能让我们很容易地对某一段代码进行性能测试。基准测试主要是通过测试 CPU 和 Memory 的效率问题,来评估被测试代码的性能,进而找到更好的解决方案。

1
2
3
4
5
6
$ go test -v -bench=.
$ go test -v -bench='Fib$' -benchtime=5s .
$ go test -v -bench='Fib$' -benchtime=1000x .
$ go test -v -bench='Fib$' -benchtime=1000x -count=5 .
$ go test -v -bench='Fib$' -cpu=2,4,8,16,32,64,128,256,512,1024,2048,4096 .
$ go test -v -benchmem -run=^$ -bench '^(BenchmarkFib)$' example.com/hello .
警告
BenchmarkXxxXxx 可以是任何字母数字字符串,但是第一个字母不能是小写字母。
1
2
3
4
5
6
func BenchmarkXxx(t *testing.T)

// 以下命名是合法的
func Benchmark123(t *testing.T)
func Benchmark中国(t *testing.T)
func BenchmarkMac(t *testing.T)

基准测试用例函数需要以 Benchmark 为前缀:

  • 前缀用例文件不会参与正常源码编译,不会被包含到可执行文件中;
  • 基准测试用例使用 go test -bench=. 指令来执行,没有也不需要 main() 作为函数入口。所有以 _test 结尾的源码内以 Benchmark 开头的函数会被自动执行;
  • 基准测试函数的参数 b *test.B 必须传入,否则会报函数签名错误,即:wrong signature for BenchmarkXxx, must be: func BenchmarkXxx(b \*testing.B)

要编写一个新的基准测试,需要创建一个名称以 _test.go 结尾的文件,该文件包含 BenchmarkXxx 函数。

待测代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package hello

func Fib(n int) int {
  switch n {
  case 0:
    return 0
  case 1:
    return 1
  default:
    return Fib(n-1) + Fib(n-2)
  }
}

测试代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package hello

import (
  "testing"
)

func BenchmarkFib(b *testing.B) {
  for n := 0; n < b.N; n++ {
    Fib(15)
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$ go test -v -benchmem -run=^$ -bench '^(BenchmarkFib)$' example.com/hello

goos: darwin
goarch: amd64
pkg: example.com/hello
cpu: Intel(R) Core(TM) i5-5257U CPU @ 2.70GHz
BenchmarkFib
BenchmarkFib-4   	  238396	      4793 ns/op	       0 B/op	       0 allocs/op
PASS
ok  	example.com/hello	2.190s

基准函数会运行目标代码 b.N 次。在基准执行期间,程序会自动调整 b.N 直到基准测试函数持续足够长的时间。b.N 对于每个用例都是不一样的。b.N1 开始,如果该用例能够在 1s 内完成,b.N 的值便会增加,再次执行。b.N 的值大概以 1, 2, 3, 5, 10, 20, 30, 50, 100 这样的序列递增,越到后面,增加得越快。

  • Benchmark 的默认时间是 1s,那么我们可以使用 -benchtime 指定为 5s
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 沿用实例一的代码做基准测试
$ go test -v -benchtime=5s -benchmem -run=^$ -bench '^(BenchmarkFib)$' example.com/hello
goos: darwin
goarch: amd64
pkg: example.com/hello
cpu: Intel(R) Core(TM) i5-5257U CPU @ 2.70GHz
BenchmarkFib
BenchmarkFib-4           1290742              4529 ns/op               0 B/op          0 allocs/op
PASS
ok      example.com/hello       10.375s

实际执行的时间是 10.375s,比 benchtime5s 要长,测试用例编译、执行、销毁等是需要时间的。

  • Benchmark-benchtime 的值除了是时间外,还可以是具体的次数:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 沿用实例一的代码做基准测试
$ go test -v -benchtime=50x -benchmem -run=^$ -bench '^(BenchmarkFib)$' example.com/hello
goos: darwin
goarch: amd64
pkg: example.com/hello
cpu: Intel(R) Core(TM) i5-5257U CPU @ 2.70GHz
BenchmarkFib
BenchmarkFib-4                50              4556 ns/op               0 B/op          0 allocs/op
PASS
ok      example.com/hello       0.010s
  • Benchmark-count 参数可以用来设置轮数:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 沿用实例一的代码做基准测试
$ go test -v -count=3 -benchmem -run=^$ -bench '^(BenchmarkFib)$' example.com/hello
goos: darwin
goarch: amd64
pkg: example.com/hello
cpu: Intel(R) Core(TM) i5-5257U CPU @ 2.70GHz
BenchmarkFib
BenchmarkFib-4            226737              4483 ns/op               0 B/op          0 allocs/op
BenchmarkFib-4            226686              5645 ns/op               0 B/op          0 allocs/op
BenchmarkFib-4            226284              4485 ns/op               0 B/op          0 allocs/op
PASS
ok      example.com/hello       3.489s
 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
package hello

import (
  "math/rand"
  "testing"
  "time"
)

func genWithCap(n int) []int {
  rand.Seed(time.Now().UnixNano())
  nums := make([]int, 0, n)
  for i := 0; i < n; i++ {
    nums = append(nums, rand.Int())
  }
  return nums
}

func genWithoutCap(n int) []int {
  rand.Seed(time.Now().UnixNano())
  nums := make([]int, 0)
  for i := 0; i < n; i++ {
    nums = append(nums, rand.Int())
  }
  return nums
}

func BenchmarkGenWithCap(b *testing.B) {
  for n := 0; n < b.N; n++ {
    genWithCap(1000000)
  }
}

func BenchmarkGenWithoutCap(b *testing.B) {
  for n := 0; n < b.N; n++ {
    genWithoutCap(1000000)
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$ go test -v -benchmem -run=^$ -bench '^BenchmarkGen' example.com/hello
goos: darwin
goarch: amd64
pkg: example.com/hello
cpu: Intel(R) Core(TM) i5-5257U CPU @ 2.70GHz
BenchmarkGenWithCap
BenchmarkGenWithCap-4                 40          28733419 ns/op         8003585 B/op          1 allocs/op
BenchmarkGenWithoutCap
BenchmarkGenWithoutCap-4              26          38608707 ns/op        45188404 B/op         40 allocs/op
PASS
ok      example.com/hello       3.167s

可以看到 genWithoutCap 分配的内存空间是 genWithCap45188404/8003585 ≈ 5.6 倍,设置了切片容量,内存只分配一次,而不设置切片容量,内存分配了 40 次。

 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
package hello

import (
  "math/rand"
  "testing"
  "time"
)

func gen(n int) []int {
  rand.Seed(time.Now().UnixNano())
  nums := make([]int, 0)
  for i := 0; i < n; i++ {
    nums = append(nums, rand.Int())
  }
  return nums
}

func helper(i int, b *testing.B) {
  for n := 0; n < b.N; n++ {
    gen(i)
  }
}

func BenchmarkGen10(b *testing.B)      { helper(10, b) }
func BenchmarkGen100(b *testing.B)     { helper(100, b) }
func BenchmarkGen1000(b *testing.B)    { helper(1000, b) }
func BenchmarkGen10000(b *testing.B)   { helper(10000, b) }
func BenchmarkGen100000(b *testing.B)  { helper(100000, b) }
func BenchmarkGen1000000(b *testing.B) { helper(1000000, b) }
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
$ go test -v -benchmem -run=^$ -bench '^(BenchmarkGen10|BenchmarkGen100|BenchmarkGen1000|BenchmarkGen10000|BenchmarkGen100000|BenchmarkGen1000000)$' example.com/hello

goos: darwin
goarch: amd64
pkg: example.com/hello
cpu: Intel(R) Core(TM) i5-5257U CPU @ 2.70GHz
BenchmarkGen10
BenchmarkGen10-4        	   85984	     12604 ns/op	     248 B/op	       5 allocs/op
BenchmarkGen100
BenchmarkGen100-4       	   89773	     14911 ns/op	    2040 B/op	       8 allocs/op
BenchmarkGen1000
BenchmarkGen1000-4      	   28988	     38968 ns/op	   16376 B/op	      11 allocs/op
BenchmarkGen10000
BenchmarkGen10000-4     	    3843	    326670 ns/op	  386296 B/op	      20 allocs/op
BenchmarkGen100000
BenchmarkGen100000-4    	     357	   3429826 ns/op	 4654346 B/op	      30 allocs/op
BenchmarkGen1000000
BenchmarkGen1000000-4   	      37	  35700196 ns/op	45188381 B/op	      40 allocs/op
PASS
ok  	example.com/hello	9.278s

通过测试结果可以发现,输入变为原来的 10 倍,函数每次调用的时长也差不多是原来的 10 倍,这说明复杂度是线性的。

基准测试中,传递给基准测试函数的参数是 *testing.B 类型。B 是传递给基准测试函数的一种类型,它用于管理基准测试的计时行为,并指示应该迭代地运行测试多少次。

跟单元测试一样,基准测试会在执行的过程中积累日志,并在测试完毕时将日志转储到标准错误。但跟单元测试不一样的是,为了避免基准测试的结果受到日志打印操作的影响,基准测试总是会把日志打印出来。

B 类型中的报告方法使用方式和 T 类型是一样的,一般来说,基准测试中也不需要使用,毕竟主要是测性能。

  • StartTimer:开始对测试进行计时。该方法会在基准测试开始时自动被调用,我们也可以在调用 StopTimer 之后恢复计时;
  • StopTimer:停止对测试进行计时。当你需要执行一些复杂的初始化操作,并且你不想对这些操作进行测量时,就可以使用这个方法来暂时地停止计时;
  • ResetTimer:对已经逝去的基准测试时间以及内存分配计数器进行清零。对于正在运行中的计时器,这个方法不会产生任何效果;
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package hello

import (
  "testing"
  "time"
)

func fib(n int) int {
  if n == 0 || n == 1 {
    return n
  }
  return fib(n-2) + fib(n-1)
}

func BenchmarkFib(b *testing.B) {
  time.Sleep(time.Second * 3)
  // b.ResetTimer()
  for n := 0; n < b.N; n++ {
    fib(30)
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// 注释 b.ResetTimer()
$ go test -v -benchtime=50x -benchmem -run=^$ -bench '^(BenchmarkFib)$' example.com/hello
goos: darwin
goarch: amd64
pkg: example.com/hello
cpu: Intel(R) Core(TM) i5-5257U CPU @ 2.70GHz
BenchmarkFib
BenchmarkFib-4   	      50	  66575387 ns/op	       1 B/op	       0 allocs/op
PASS
ok  	example.com/hello	6.358s

// 打开 b.ResetTimer()
$ go test -v -benchtime=50x -benchmem -run=^$ -bench '^(BenchmarkFib)$' example.com/hello
goos: darwin
goarch: amd64
pkg: example.com/hello
cpu: Intel(R) Core(TM) i5-5257U CPU @ 2.70GHz
BenchmarkFib
BenchmarkFib-4   	      50	   6419112 ns/op	       0 B/op	       0 allocs/op
PASS
ok  	example.com/hello	6.344s

可以看到,当注释 b.ResetTimer() 后,每次执行需要 66575387/1000000000=0.06657539≈0.067 秒;当打开 b.ResetTimer() 后,每次执行需要 6419112/1000000000=0.00641911≈0.006 秒。所以使用 b.ResetTimer() 重置定时器后快了 0.067/0.006=11.16666667≈11 倍。

在某些情况下,每次调用函数前后需要一些准备工作和清理工作,可以使用 StopTimer 暂停计时以及使用 StartTimer 开始计时:

 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
package hello

import (
  "math/rand"
  "testing"
  "time"
)

func genWithCap(n int) []int {
  rand.Seed(time.Now().UnixNano())
  nums := make([]int, 0, n)
  for i := 0; i < n; i++ {
    nums = append(nums, rand.Int())
  }
  return nums
}

func bubbleSort(nums []int) {
  for i := 0; i < len(nums); i++ {
    for j := 1; j < len(nums)-i; j++ {
      if nums[j] < nums[j-1] {
        nums[j], nums[j-1] = nums[j-1], nums[j]
      }
    }
  }
}

func BenchmarkBubbleSort(b *testing.B) {
  for n := 0; n < b.N; n++ {
    b.StopTimer()
    nums := genWithCap(10000)
    b.StartTimer()
    bubbleSort(nums)
  }
}
1
2
3
4
5
6
7
8
9
$ go test -v -benchtime=50x -benchmem -run=^$ -bench '^(BenchmarkBubbleSort)$' example.com/hello
goos: darwin
goarch: amd64
pkg: example.com/hello
cpu: Intel(R) Core(TM) i5-5257U CPU @ 2.70GHz
BenchmarkBubbleSort
BenchmarkBubbleSort-4   	      50	 124283402 ns/op	       0 B/op	       0 allocs/op
PASS
ok  	example.com/hello	6.380s

通过 RunParallel 方法能够并行地执行给定的基准测试。RunParallel 会创建出多个 goroutine,并将 b.N 分配给这些 goroutine 执行,其中 goroutine 数量的默认值为 GOMAXPROCS。用户如果想要增加非 CPU 受限(non-CPU-bound)基准测试的并行性,那么可以在 RunParallel 之前调用 SetParallelism(如:SetParallelism(2),则 goroutine 数量为 2*GOMAXPROCS)。RunParallel 通常会与 -cpu 标志一同使用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package hello

import (
  "bytes"
  "testing"
  "text/template"
)

func BenchmarkTemplateParallel(b *testing.B) {
  tpl := template.Must(template.New("test").Parse("Hello, {{.}}!"))
  // RunParallel will create GOMAXPROCS goroutines and distribute work among them.
  b.RunParallel(func(pb *testing.PB) {
    // Each goroutine has its own bytes.Buffer.
    var buf bytes.Buffer
    for pb.Next() {
    // The loop body is executed b.N times total across all goroutines.
      buf.Reset()
      tpl.Execute(&buf, "World")
    }
  })
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$ go test -v -benchmem -run=^$ -bench '^(BenchmarkTemplateParallel)$' example.com/hello

goos: darwin
goarch: amd64
pkg: example.com/hello
cpu: Intel(R) Core(TM) i5-5257U CPU @ 2.70GHz
BenchmarkTemplateParallel
BenchmarkTemplateParallel-4   	 7044910	       198.2 ns/op	      48 B/op	       1 allocs/op
PASS
ok  	example.com/hello	1.581s
 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
package hello

import (
  "testing"
)

func BenchmarkSelectNonblock(b *testing.B) {
  ch1 := make(chan int)
  ch2 := make(chan int)
  ch3 := make(chan int, 1)
  ch4 := make(chan int, 1)
  b.RunParallel(func(pb *testing.PB) {
    for pb.Next() {
      select {
        case <-ch1:
        default:
      }
      select {
        case ch2 <- 0:
        default:
      }
      select {
        case <-ch3:
        default:
      }
      select {
        case ch4 <- 0:
        default:
      }
    }
  })
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$ go test -v -benchmem -run=^$ -bench '^(BenchmarkSelectNonblock)$' example.com/hello

goos: darwin
goarch: amd64
pkg: example.com/hello
cpu: Intel(R) Core(TM) i5-5257U CPU @ 2.70GHz
BenchmarkSelectNonblock
BenchmarkSelectNonblock-4   	96872894	        13.02 ns/op	       0 B/op	       0 allocs/op
PASS
ok  	example.com/hello	1.286s

ReportAllocs 方法用于打开当前基准测试的内存统计功能, 与 go test 使用 -benchmem 标志类似,但 ReportAllocs 只影响那些调用了该函数的基准测试。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package hello

import (
  "bytes"
  "html/template"
  "testing"
)

func BenchmarkTmplExucte(b *testing.B) {
  b.ReportAllocs()
  tpl := template.Must(template.New("test").Parse("Hello, {{.}}!"))
  b.RunParallel(func(pb *testing.PB) {
    // Each goroutine has its own bytes.Buffer.
    var buf bytes.Buffer
    for pb.Next() {
      // The loop body is executed b.N times total across all goroutines.
      buf.Reset()
      tpl.Execute(&buf, "World")
    }
  })
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$ go test -v -benchmem -run=^$ -bench '^(BenchmarkTplExucte)$' example.com/hello

goos: darwin
goarch: amd64
pkg: example.com/hello
cpu: Intel(R) Core(TM) i5-5257U CPU @ 2.70GHz
BenchmarkTmplExucte
BenchmarkTmplExucte-4
 1525129	       875.6 ns/op	     240 B/op	       8 allocs/op
PASS
ok  	example.com/hello	2.152s
1
2
// 循环执行了 238396 次,每次循环花费 4793 ns
BenchmarkFib-4   	  238396	      4793 ns/op	       0 B/op	       0 allocs/op
  • BenchmarkFib-4BenchmarkFib-8 中的 -8GOMAXPROCS,默认等于 CPU 核数。可以通过 -cpu 参数改变 GOMAXPROCS-cpu 支持传入一个列表作为参数(比如:-cpu=2,4,8,...);
  • 238396:基准测试的迭代总次数 b.N
  • 4793 ns/op:平均每次迭代所消耗的纳秒数;
  • 0 B/op:平均每次迭代内存所分配的字节数;
  • 0 allocs/op:平均每次迭代的内存分配次数;

testing 包中的 BenchmarkResult 类型保存了基准测试的结果,定义如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// BenchmarkResult contains the results of a benchmark run.
type BenchmarkResult struct {
  N         int           // The number of iterations.
  T         time.Duration // The total time taken.
  Bytes     int64         // Bytes processed in one iteration.
  MemAllocs uint64        // The total number of memory allocations.
  MemBytes  uint64        // The total number of bytes allocated.

  // Extra records additional metrics reported by ReportMetric.
  Extra map[string]float64
}
 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
package main

import (
	"bytes"
	"fmt"
	"testing"
	"text/template"
)

func main() {
  res := testing.Benchmark(func(b *testing.B) {
    tpl := template.Must(template.New("test").Parse("Hello, {{.}}!"))
    b.RunParallel(func(pb *testing.PB) {
      var buf bytes.Buffer
      for pb.Next() {
        buf.Reset()
        tpl.Execute(&buf, "World")
      }
    })
  })
  fmt.Printf("%8d\t%10d ns/op\t%10d B/op\t%10d allocs/op\n",
    res.N,
    res.NsPerOp(),
    res.AllocedBytesPerOp(),
    res.AllocsPerOp(),
  )
}
1
2
$ go run main.go
 5220064	       196 ns/op	        48 B/op	         1 allocs/op