概述

在 Go 语言的自带测试集中,就默认地提供了 Benchmark 的工具,本文介绍的就是这个 Benchmark 工具,包括它的测试原理以及一些相对复杂的测试实现。

简单的 benchmark

一个简单的 benchmark 真的就非常简单,例如:

  1. [root@liqiang.io]# cat benchmark_test.go
  2. func BenchmarkTest(b *testing.B) {
  3. for i := 0; i < b.N; i++ {
  4. _ = add(1, 2)
  5. }
  6. }
  7. func add(a, b int) int {
  8. return a + b
  9. }

然后运行一下看看:

  1. [root@liqiang.io]# go test -test.bench ^BenchmarkTest$ -test.run ^$ .
  2. goos: darwin
  3. goarch: amd64
  4. pkg: xxxxxx
  5. cpu: xxxxxx
  6. BenchmarkTest-12 1000000000 0.2499 ns/op
  7. PASS
  8. ok xxxxxx 0.402s

可以看到,这里返回了一行:

  1. BenchmarkTest-12 1000000000 0.2499 ns/op

第一列就是表示 benchmark 的是哪个函数,第二列表示运行了这个函数多少次(b.N 中的 N),然后最后一个就是 benchmark 的结果,这里每次调用的时间是 0.2499 ns,非常地快。那么可以看到其他 benchmark 吗?例如时间可能因 cpu 的不同或者机器任务的繁忙程度有所不同;此外,很多时候,我们看一段代码速度快不快是一方面,内存占用高不高也是一个很关键的因素,还有 IO 是否频繁也是需要考虑的点(当然,IO 可能反映在速度上),所以这里我们可以加上一个内存相关的分析,我稍微修改一下代码,然后看结果就好了,注意,这里的运行参数我加多了一个:-benchmem

  1. [root@liqiang.io]# go test -test.bench ^BenchmarkTest$ -test.run ^$ . -benchmem
  2. goos: darwin
  3. goarch: amd64
  4. pkg: xxxxxx
  5. cpu: xxxxxx
  6. BenchmarkTest-12 100000000 10.30 ns/op 3 B/op 1 allocs/op
  7. PASS
  8. ok xxxxxx 2.034s

因为我修改了代码,所以时间我们就忽略了,可以发现,在结果中多两列,分别是每次操作(for 里面调用的函数)分配了多少内存,以及每次操作申请了多少次内存。

自定义报告指标

内置的指标就这么几个,如果你觉得还不够,没问题,你可以自己添加指标,例如你想 benchmark 一下自带的排序算法的复杂度是多少,那么就是看在排序的时候比较了多少次,那么可以这么看:

  1. [root@liqiang.io]# cat sort_slice_test.go
  2. func BenchmarkTest(b *testing.B) {
  3. compare := 0
  4. for i := 0; i < b.N; i++ {
  5. sortSlice := []int{1, 2, 3, 4, 5}
  6. sort.Slice(sortSlice, func(i, j int) bool {
  7. compare++
  8. return sortSlice[i] < sortSlice[j]
  9. })
  10. }
  11. b.ReportMetric(float64(compare)/float64(b.N), "compare/op")
  12. }

然后运行一下看看结果:

  1. [root@liqiang.io]# go test -test.bench ^BenchmarkTest$ -test.run ^$ . -benchmem
  2. goos: darwin
  3. goarch: amd64
  4. pkg: xxxxxxx
  5. cpu: xxxxxxx
  6. BenchmarkTest-12 8820426 123.3 ns/op 4.000 compare/op 104 B/op 3 allocs/op
  7. PASS
  8. ok xxxxxxx 2.139s

横向比较

很多时候,我们做 benchmark 其实都是用于比较,例如我们自己写了一个 SDK,然后想和其他相同类型的 SDK 做个性能对比,那么繁琐一点我们可以手动运行多次,每次运行一个 SDK 的 benchmark,然后比较这多次运行的结果;对于我来说,我更想的是,一次运行多个 SDK 的 benchmark,然后直接看多个 SDK 的类似操作的结果,那么可以这么做:

  1. [root@liqiang.io]# cat multi_bench_test.go
  2. func BenchmarkSDK1(b *testing.B) {
  3. for i := 0; i < b.N; i++ {
  4. SDK1.func()
  5. }
  6. }
  7. func BenchmarkSDK2(b *testing.B) {
  8. for i := 0; i < b.N; i++ {
  9. SDK2.func()
  10. }
  11. }

看上去是可行的,但是关系不是那么强,我更希望是类似这样的操作:

  1. [root@liqiang.io]# cat multi_bench_test.go
  2. func BenchmarkFunc(b *testing.B) {
  3. for i := 0; i < b.N; i++ {
  4. SDK1.func()
  5. }
  6. for i := 0; i < b.N; i++ {
  7. SDK2.func()
  8. }
  9. }

但是,如果你直接这么写,你会发现结果不是你想要的,我这里给个例子:

  1. [root@liqiang.io]# cat multi_bench_test.go
  2. func BenchmarkAdd1(b *testing.B) {
  3. for i := 0; i < b.N; i++ {
  4. add(3, 5)
  5. }
  6. }
  7. func BenchmarkAdd2(b *testing.B) {
  8. for i := 0; i < b.N; i++ {
  9. add2(3, 5)
  10. }
  11. }
  12. func BenchmarkTwoAdd(b *testing.B) {
  13. for i := 0; i < b.N; i++ {
  14. add2(3, 5)
  15. }
  16. for i := 0; i < b.N; i++ {
  17. add(3, 5)
  18. }
  19. }

运行一下,结果是这样的,其实就是将两个结果加起来了:

  1. [root@liqiang.io]# go test -test.bench '.*' -test.run ^$ . -benchmem
  2. goos: darwin
  3. goarch: amd64
  4. pkg: xxxxxxxxx
  5. cpu: xxxxxxxxx
  6. BenchmarkAdd1-12 50087434 21.98 ns/op 10 B/op 2 allocs/op
  7. BenchmarkAdd2-12 100000000 10.84 ns/op 5 B/op 1 allocs/op
  8. BenchmarkTwoAdd-12 36373838 32.60 ns/op 16 B/op 3 allocs/op
  9. PASS
  10. ok xxxxxxxxx 3.579s

正确的写法

正确的写法是这样的:

  1. [root@liqiang.io]# cat multi_bench_test.go
  2. func BenchmarkTwoAdd(b *testing.B) {
  3. var funcs = []func(a, b int) int{add, add2}
  4. for idx, f := range funcs {
  5. b.Run("function "+strconv.Itoa(idx), func(b *testing.B) {
  6. for i := 0; i < b.N; i++ {
  7. f(3, 5)
  8. }
  9. })
  10. }
  11. }

然后我们运行一下看结果:

  1. [root@liqiang.io]# go test -test.bench '^BenchmarkTwoAdd$' -test.run ^$ . -benchmem
  2. goos: darwin
  3. goarch: amd64
  4. pkg: xxxxxxxx
  5. cpu: xxxxxxxx
  6. BenchmarkTwoAdd/function_0-12 44406841 24.75 ns/op 10 B/op 2 allocs/op
  7. BenchmarkTwoAdd/function_1-12 90792690 12.98 ns/op 5 B/op 1 allocs/op
  8. PASS
  9. ok xxxxxxxx 3.325s

可以发现这就不是两个函数调用相加了,而是独立的两个函数,但是可以发现,这里的运行时间比单独运行稍微多了一些,但是无伤大雅,我们关注的是横向对比,这里就很明确了。

小结

OK,这就是关于 Go 自带的 Benchmark 的一些介绍和总结了,希望对你有所帮助。

Ref