本文章及以下示例均运行于windows环境

前言

开始进行benchmark性能测试之前,需要注意:

  • 基准测试的代码文件必须以_test.go结尾
  • 基准测试的函数必须以Benchmark开头,必须是可导出的
  • 基准测试函数必须接受一个指向Benchmark类型的指针作为唯一参数
  • 基准测试函数不能有返回值
  • 最后的for循环很重要,被测试的代码要放到循环里
  • b.N是基准测试框架提供的,表示循环的次数,因为需要反复调用测试的代码,才可以评估性能

简单使用

一个简单的benchtest用例

package main

import (
    "testing"
)

func BenchmarkContactStr(b *testing.B) {
    for i := 0; i < b.N; i++ {
        str := "a"
        str += "bc"
    }
}

命令行执行:

go test -bench .

其中-bench .则为参数,仅测试bench的函数(Benchmark开头),后面空格加一个.,则为测试全部

运行结果:

goos: windows
goarch: amd64
BenchmarkContactStr-12          34332700                31.6 ns/op
PASS
ok      _/C_/Users/admin/Desktop/test      1.319s

其中,BenchmarkContactStr为测试的函数名,函数名后的-12为运行时对应的GOMAXPROCS的值(逻辑CPU数量),34332700表示测试循环的次数,ns/op表示每一个操作耗费多少时间(纳秒)

基准测试框架对一个测试用例的默认测试时间是 1 秒。开始测试时,当以 Benchmark 开头的基准测试用例函数返回时还不到 1 秒,那么 testing.B 中的 N 值将按 1、2、5、10、20、50……递增,同时以递增后的值重新调用基准测试用例函数。

常用命令行参数

  • -bench grep:通过正则表达式过滤出需要进行benchtest的用例
  • -count n:跑n次benchmark,n默认为1
  • -benchmem:打印内存分配的信息
  • -benchtime=5s:自定义测试时间,默认为1s
  • -run regexp:只运行特定的测试函数,比如-run ABC只测试函数名中包含ABC的测试函数
  • -timeout t:测试时间如果超过t, panic,默认10分钟
  • -v:显示测试的详细信息,也会把Log、Logf方法的日志显示出来
查看benchtest的参数: go help testflag

查看内存分配

命令行执行:

go test -bench . -benchmem

运行结果:

goos: windows
goarch: amd64
BenchmarkContactStr-12          38319422                32.7 ns/op             3 B/op          1 allocs/op
PASS
ok      _/C_/Users/admin/Desktop/test      2.471s

其中,BenchmarkContactStr为测试的函数名,函数名后的-12为运行时对应的GOMAXPROCS的值(逻辑CPU数量),38319422表示测试循环的次数,ns/op表示每一个操作耗费多少时间(纳秒),B/op表示每次调用需要分配的字节数量。allocs/op表示每次调用有多少次分配

benchmark常用API

  • b.StopTimer()
  • b.StartTimer()
  • b.ResetTimer()
  • b.RunParallel(body func(*PB))

b.RunParallel

串行用法

func BenchmarkContactStr(b *testing.B) {
    for i := 0; i < b.N; i++ {
        str := "a"
        str += "bc"
    }
}

最基本用法,for循环中的执行过程在达到1秒或超过1秒时,总共执行多少次。b.N的值就是最大次数。

并行用法

func BenchmarkContactStr(b *testing.B) {
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            str := "a"
            str += "bc"
        }
    })
}

如果代码只是像上例这样写,那么并行的goroutine个数是默认等于runtime.GOMAXPROCS(0)

创建P个goroutine之后,再把b.N打散到每个goroutine上执行,所以并行用法就比较适合IO型的测试对象

若想增大goroutine的个数,那就使用b.SetParallelism(p int)

// 最终goroutine个数 = 形参p的值 * runtime.GOMAXPROCS(0)
numProcs := b.parallelism * runtime.GOMAXPROCS(0)
b.SetParallelism()的调用一定要放在b.RunParallel()之前

并行用法带来一些启示,注意到b.N是被RunParallel()接管的。意味着,开发者可以自己写一个RunParallel()方法,goroutine个数和b.N的打散机制自己控制。或接管b.N之后,定制自己的策略。

要注意b.N会递增,这次b.N执行完,不满足终止条件,就会递增b.N,逼近上限,直至满足终止条件

// 终止策略: 执行过程中没有竟态问题 & 时间没超出 & 次数没达到上限
// d := b.benchTime
if !b.failed && b.duration < d && n < 1e9 {}

Start/Stop/ResetTimer

这三个都是对 计时统计器 和 内存统计器 操作。

benchmark中难免有一些初始化的工作,这些工作耗时不希望被计算进benchmark结果中

ResetTimer

// 串行情况在for循环之前调用
func BenchmarkContactStr(b *testing.B) {
    // do something...   初始化
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        str := "a"
        str += "bc"
    }
}

// 并行情况在b.RunParallel()之前调用
func BenchmarkContactStr(b *testing.B) {
    // do something...   初始化
    b.ResetTimer()
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            str := "a"
            str += "bc"
        }
    })
}

Start/StopTimer

func BenchmarkContactStr(b *testing.B) {
    // do something...   初始化
    b.ResetTimer()
    // do something...   某些过程需要计算性能
    b.StopTimer()
    // do something...   某些过程不需要计算性能
    b.StartTimer()
    for i := 0; i < b.N; i++ {
        str := "a"
        str += "bc"
    }
}
关于更详尽的用法请查阅官方文档

结合 pprof性能监控

使用简单使用中的用例

同时查看内存与性能:

go test -bench . -benchmem -memprofile memprofile.out -cpuprofile profile.out

执行后会在当前目录生成memprofile.out以及profile.out2个文件

然后就可以用输出的文件使用pprof

查看性能

首先执行

go tool pprof profile.out

然后命令行输出:

Type: cpu
Time: Feb 28, 2020 at 4:01pm (CST)
Duration: 1.31s, Total samples = 1.32s (100.85%)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof)

然后输入top

Type: cpu
Time: Feb 28, 2020 at 4:01pm (CST)
Duration: 1.31s, Total samples = 1.32s (100.85%)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top 
Showing nodes accounting for 1140ms, 86.36% of 1320ms total
Showing top 10 nodes out of 61
      flat  flat%   sum%        cum   cum%
     360ms 27.27% 27.27%     1020ms 77.27%  runtime.concatstrings
     210ms 15.91% 43.18%      390ms 29.55%  runtime.mallocgc
     170ms 12.88% 56.06%      170ms 12.88%  runtime.memmove
      90ms  6.82% 62.88%     1110ms 84.09%  runtime.concatstring2
      70ms  5.30% 68.18%      490ms 37.12%  runtime.rawstringtmp
      60ms  4.55% 72.73%       60ms  4.55%  runtime.releasem
      60ms  4.55% 77.27%       60ms  4.55%  runtime.stdcall1
      50ms  3.79% 81.06%       50ms  3.79%  runtime.acquirem
      40ms  3.03% 84.09%     1150ms 87.12%  _/C_/Users/admin/Desktop/test.BenchmarkContactStr
      30ms  2.27% 86.36%       30ms  2.27%  runtime.gomcache

可以看到cpu时间,可以继续执行list BenchmarkContactStr(list + 测试函数名)

Type: cpu
Time: Feb 28, 2020 at 4:01pm (CST)
Duration: 1.31s, Total samples = 1.32s (100.85%)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) list BenchmarkContactStr
Total: 1.32s
ROUTINE ======================== _/C_/Users/admin/Desktop/test.BenchmarkContactStr in C:\Users\admin\Desktop\test\benchmark_test.go
      40ms      1.15s (flat, cum) 87.12% of Total
         .          .      3:import (
         .          .      4:   "testing"
         .          .      5:)
         .          .      6:
         .          .      7:func BenchmarkContactStr(b *testing.B) {
      10ms       10ms      8:   for i := 0; i < b.N; i++ {
         .          .      9:           str := "a"
      30ms      1.14s     10:           str += "bc"
         .          .     11:   }
         .          .     12:}

以上则可以看到用例每一步执行的时间

查看内存占用

首先执行

go tool pprof memprofile.out

然后命令行输出:

Type: alloc_space
Time: Feb 28, 2020 at 4:01pm (CST)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof)

然后输入top

Type: alloc_space
Time: Feb 28, 2020 at 4:01pm (CST)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top
Showing nodes accounting for 121.38MB, 99.12% of 122.46MB total
Dropped 8 nodes (cum <= 0.61MB)
      flat  flat%   sum%        cum   cum%
  120.50MB 98.40% 98.40%   120.50MB 98.40%  _/C_/Users/admin/Desktop/test.BenchmarkContactStr
    0.88MB  0.72% 99.12%     1.45MB  1.18%  compress/flate.NewWriter
         0     0% 99.12%     1.95MB  1.60%  compress/gzip.(*Writer).Write
         0     0% 99.12%     1.95MB  1.60%  runtime/pprof.(*profileBuilder).build
         0     0% 99.12%     1.95MB  1.60%  runtime/pprof.(*profileBuilder).flush
         0     0% 99.12%     1.95MB  1.60%  runtime/pprof.(*profileBuilder).locForPC
         0     0% 99.12%     1.95MB  1.60%  runtime/pprof.profileWriter
         0     0% 99.12%   120.50MB 98.40%  testing.(*B).launch
         0     0% 99.12%   120.50MB 98.40%  testing.(*B).runN

可以看到内存占用,可以继续执行list BenchmarkContactStr(list + 测试函数名)

Type: alloc_space
Time: Feb 28, 2020 at 4:01pm (CST)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) list BenchmarkContactStr
Total: 122.46MB
ROUTINE ======================== _/C_/Users/admin/Desktop/test.BenchmarkContactStr in C:\Users\admin\Desktop\test\benchmark_test.go
  120.50MB   120.50MB (flat, cum) 98.40% of Total
         .          .      5:)
         .          .      6:
         .          .      7:func BenchmarkContactStr(b *testing.B) {
         .          .      8:   for i := 0; i < b.N; i++ {
         .          .      9:           str := "a"
  120.50MB   120.50MB     10:           str += "bc"
         .          .     11:   }
         .          .     12:}

以上则可以看到用例每一步的内存占用

测试注意和调优

  • 避免频繁调用timer
  • 避免测试数据过大

参考文档

go benchmark实践与原理
go benchmark 性能测试
Benchtest的简单使用
Go语言标准库文档中文版-testing

Last modification:February 28, 2020
If you think my article is useful to you, please feel free to appreciate