引言

本文偏手册性质,需求写 benchmark 时,希望能经过本文快速上手基准测验。

Benchmark是Go中一个特殊的函数,和单元测验相似,首要意图就是测验代码的功能,常常运用的首要有2种:

  • 运用 b.N 测验某个函数的耗时和内存分配状况。
  • 运用 b.RunParallel() 运用多核CPU测验某个函数的并发状况。

咱们经过2个比方,来别离介绍一下这2种干流用法。

入门比方

简介

以leetcode 509斐波那契数为例,咱们完结了一种最简略的递归版别的解法,现在假定咱们要编写 Benchmark 来测验这种解法的功能,让咱们对算法的好坏有一个直观的了解。

1.创建一个go mod项目

$ mkdir example && cd example
$ go mod init example

2.新建一个 fib.go 文件,完结递归解法:

$ vim fib.go
package main
func Fib(n int) int {
if n == 0 || n == 1 {
return n
}
return Fib(n-2) + Fib(n-1)
}

3.然后和单元测验相同,需求新建一个同名以test结束的文件 fib_test.go:

$ vim fib_test.go

4.写下如下基准测验代码:

package main
import "testing"
func BenchmarkFib(b *testing.B) {
for n := 0; n < b.N; n++ {
Fib(30) // 重复运转Fib(30)函数b.N次
}
}
  • 留意函数名以 Benchmark 开头,参数是 b *testing.B。和普通的单元测验用例很像,单元测验函数名以 Test 开头,参数是 t *testing.T。
  • b.N:循环次数,假如函数运转足够快,下一次 go test 调用BenchmarkFib时,b.N的值最多以100倍增长(1、100、10000次……),具体参见go源码中(testing/benchmark.go:lanuch)函数中的算法。
  • Fib(30):核算第30个斐波那契数列

运转用例

经过如下指令运转基准测验:

$ go test -bench="."

此刻,输出如下结果:

goos: darwin
goarch: arm64
pkg: goexample/13_benchmark
BenchmarkFib-103163636114 ns/op
PASS
okgoexample/13_benchmark1.969s
  • BenchmarkFib-10:”-10″ 表明发动了10个cpu履行测验,可是由于 BenchmarkFib 函数是单协程运转,故和一个cpu履行的作用相同。
  • 316:代表BenchmarkFib 在1秒内履行了316次,经过 -benchtime 能够改变测验时长。
  • 3636114 ns/op:函数履行的平均耗时,纳秒单位,除以1000*1000后约3.63 ms,由于有一些发动初始化等工作,所以:耗时 * 次数 > 1秒 是正常现象。

benchmark 是怎么工作的

benchmark用例的参数 b *testing.B 中,有一个属性 b.N ,代表用例中测验代码循环的次数。

运转测验时,b.N会先从1开端,假如该用例能在1s内完结,阐明函数足够快,则go test 会依据一定的规矩添加 b.N并再次运转该用例,最多以100倍的速度添加,最多以100倍的速度添加(go1.19)。

咱们加个log试验下:

func BenchmarkFib(b *testing.B) {
b.Log("BenchmarkFib, b.N=", b.N)
for n := 0; n < b.N; n++ {
Fib(30) // 重复运转Fib(30)函数b.N次
}
}

输出:

$ go test -run=none -bench="BenchmarkFib$"
goos: darwin
goarch: arm64
pkg: goexample/13_benchmark
BenchmarkFib-103213687196 ns/op
--- BENCH: BenchmarkFib-10
fib_test.go:9: BenchmarkFib, b.N= 1
fib_test.go:9: BenchmarkFib, b.N= 100
fib_test.go:9: BenchmarkFib, b.N= 321
PASS
okgoexample/13_benchmark1.958s

阐明用例 BenchmarkFib 1秒内被调用了3次,第一次b.N=1,第2次b.N=100,第三次b.N=321,此刻时刻耗尽,整个测验履行完结。

测验时刻复杂度

经过上面的比方,咱们发现 Fib(30) 函数运转一次需求 3.6ms,感觉有点慢。此刻,咱们能够持续添加位数,比方核算第 40 位的数列,来观察函数的时刻复杂度:

func benchFib(b *testing.B, num int) {
for n := 0; n < b.N; n++ {
Fib(num)
}
}
func BenchmarkFib_10(b *testing.B) {
benchFib(b, 10)
}
func BenchmarkFib_30(b *testing.B) {
benchFib(b, 30)
}
func BenchmarkFib_40(b *testing.B) {
benchFib(b, 40)
}

运转后输出:

$go test -run=none -bench="BenchmarkFib_"
goos: darwin
goarch: arm64
pkg: goexample/13_benchmark
BenchmarkFib_10-104688688244.3 ns/op
BenchmarkFib_30-103213730468 ns/op
BenchmarkFib_40-103455273528 ns/op

发现了吗?核算 Fib(40) 居然需求 455ms,假如核算第 60 位数字,不知道要多久,运转出来的同学评论区留言一下。

也就是说,递归解法的功能下降趋势超越了指数级(4688688 -> 321 -> 3)。时刻复杂度网上的一种说法是O(2^n),另一种说法我打不出来,感兴趣的能够查一下。

咱们换另一个迭代版别 O(n) 复杂度的解法试试:

// FibIterator 迭代版别解法,O(n)时刻复杂度,算法功能大幅度提高
func FibIterator(n int) int {
if n <= 1 {
return n
}
// 空间换时刻,把前一个结果缓存起来,避免重复核算
var n2, n1 = 0, 1
for i := 2; i < n; i++ {
n2, n1 = n1, n1+n2
}
return n2 + n1
}

再次运转:

BenchmarkFib_10-10  303285657   3.821 ns/op
BenchmarkFib_30-10  100000000   10.22 ns/op
BenchmarkFib_40-10  89248639   13.44 ns/op

咱们看到,跟着核算数字的增大,耗时线性增长,且第40位数字核算只花费了 13.44 ns,功能提高了:455273528 / 13.44,大约 3300万倍

Benchmark指令简介

语法

go test指令用来运转某个 package 内除 Benchmark 测验代码之外的 一切测验用例

$ go test <module name>/<package name>  # module name: go mod项目名,能够省略
$go test .               # 运转当前 package 内的一切用例,能够省略 "."

所以,这也是上文中为什么需求咱们额定指定 -bench指令的原因(否则不会运转基准测验):

$ go test -bench="."

等价于下面的指令:

$ go test -run="." -bench="."
  • -run regexp: 运转一切正则匹配的 tests, examples, fuzz tests 等类型的测验(函数名)。在正则中”.” 表明一切字符串,故会运转一切单元测验,来历:官网
  • -bench regexp:除运转-run匹配的测验之外,额定依据正则匹配结果,运转一切匹配的 benchmarks 测验,相同这儿也是匹配一切 benchmark 函数名,即运转一切基准测验。更多正则语法,请参考:w3cschool

比方,咱们能够运用 “Fib$” 只运转以Fib关键字结束的 Benchmark(留意单测也会运转,假如有的话):

$go test -bench="Fib$"

输出:

goos: darwin
goarch: arm64
pkg: goexample/13_benchmark
BenchmarkFib-103153669052 ns/op
PASS
okgoexample/13_benchmark1.968s

需求留意的是,假如当前package下还有其他 tests 单元测验,会一并运转,能够经过 -v 指令检查是否履行以及履行了那些单测。

越过unit test

$ go test -run="none" -bench="BenchmarkFib"

咱们只需求给 -run 中指定一个不匹配任何单测的正则,即可越过单测,只运转 benchmark测验。

常用正则

为了便利咱们,这儿附上几个常用正则的含义:

  • .:匹配一切字符串
  • $:匹配输入字符串的结束方位
  • *:匹配前面的子表达式零次或屡次
  • ^:匹配输入字符串的开端方位

另外,搭配在线东西能更高效的写出正确的正则(tool.lu/regex/):

Go-Benchmark入门-基础篇(上)

Go-Benchmark入门-基础篇(上)

高档指令

完好指令参加官方文档:Testing Flags

常用的高档指令如下:

-cpu 1,2,4
Specify a list of GOMAXPROCS values for which the tests, benchmarks or
fuzz tests should be executed. The default is the current value
of GOMAXPROCS. -cpu does not apply to fuzz tests matched by -fuzz.
-benchtime t
Run enough iterations of each benchmark to take t, specified
as a time.Duration (for example, -benchtime 1h30s).
The default is 1 second (1s).
The special syntax Nx means to run the benchmark N times
(for example, -benchtime 100x).
-benchmem
Print memory allocation statistics for benchmarks.
-count n
Run each test, benchmark, and fuzz seed n times (default 1).
If -cpu is set, run n times for each GOMAXPROCS value.
Examples are always run once. -count does not apply to
fuzz tests matched by -fuzz.
-cpuprofile cpu.out
 Write a CPU profile to the specified file before exiting.
Writes test binary as -c would.
-memprofile mem.out
Write an allocation profile to the file after all tests have passed.
Writes test binary as -c would.

高档比方

b.Run子测验

上文中,咱们为了测验不同输入下,斐波拉契算法的耗时,写了如下代码:

func benchFib(b *testing.B, num int) {
for n := 0; n < b.N; n++ {
FibIterator(num)
}
}
func BenchmarkFib_10(b *testing.B) {
benchFib(b, 10)
}
func BenchmarkFib_30(b *testing.B) {
benchFib(b, 30)
}
func BenchmarkFib_40(b *testing.B) {
benchFib(b, 40)
}

实际上,咱们能够运用 b.Run 运转子测验,把代码合并成:

func BenchmarkFib_Table(b *testing.B) {
var table = []int{10, 30, 40}
for i := 0; i < len(table); i++ {
num := table[i]
name := fmt.Sprintf("%s_%d", "BenchmarkFib", num)
b.Run(name, func(b *testing.B) {
benchFib(b, num)
})
}
}

运转后输出:

$ go test -run=none -bench="BenchmarkFib_Table$"
goos: darwin
goarch: arm64
pkg: goexample/13_benchmark
BenchmarkFib_Table/BenchmarkFib_10-103018529043.781 ns/op
BenchmarkFib_Table/BenchmarkFib_30-1010000000010.12 ns/op
BenchmarkFib_Table/BenchmarkFib_40-108988511413.29 ns/op
PASS
okgoexample/13_benchmark4.271s

达到了相同的作用,且代码更简洁!相似单测中的表格驱动测验法。

b.RunParallel 并发测验

通常状况下,b.N 用来测验函数的履行耗时,而 b.RunParallel 看姓名就知道是为了测验不同 CPU 状况下,函数的并发次数。

以mysql场景举例,假定我要测验创建群组函数的QPS:

func (p pushGroup) Create(ctx context.Context, group *model.BizPushGroup) error {
val, err := p.client.BizPushGroup.Query().Where(bizpushgroup.BizUUID(group.BizUUID)).Only(ctx)
if ent.IsNotFound(err) {
val, err = p.client.BizPushGroup.Create().SetBizUUID(group.BizUUID).Save(ctx)
if err != nil {
return err
}
} else if val != nil && val.Status { // 已删除,康复
err = p.client.BizPushGroup.Update().SetStatus(false).Where(bizpushgroup.ID(val.ID)).Exec(ctx)
if err != nil {
return err
}
}
group.ID = val.ID
return nil
}

咱们的 benchmark 代码如下:

func BenchmarkPushGroup_Create(b *testing.B) {
entClient := newEntClient(b)
redisCli := unittest.NewRedis(b)
group := NewPushGroupDao(entClient, redisCli, NewPushGroupMemberDao(entClient, redisCli))
 // 疏忽连接mysql等初始化耗时
b.ResetTimer()
rand.Seed(time.Now().Unix())
key := fmt.Sprintf("benchmark-group-%d", rand.Int())
num := atomic.Int32{}
 // 发动-cpu 1,2,4 指令中指定个数的routine,而且同时履行
b.RunParallel(func(pb *testing.PB) {
   // 留意,这儿不再是判断 b.N,而是经过 pb.Next() 确认是否需求持续运转测验
for pb.Next() {
num.Inc()
group.Create(context.Background(), &model.BizPushGroup{
BizUUID: fmt.Sprintf("%s-%d", key, num.Load()),
})
}
})
}
  • b.RunParallel() 中会在一个go routine中履行,直到 pb.Next() 变成false停止,这个由go tool东西操控。
  • b.ResetTimer() 显现疏忽初始化的耗时,除此之外,还能够运用StopTimer 和StartTimer 准确疏忽某一段代码的耗时。

运转时,咱们能够经过 -cpu 指令操控cpu个数(不指定则默许机器的cpu个数):

$go test -run=none -bench="BenchmarkPushGroup_Create" -cpu 1,2,4
goos: darwin
goarch: arm64
pkg: git.shuodev.com/server/msg-dispatcher/internal/dao
BenchmarkPushGroup_Create2726760698 ns/op
BenchmarkPushGroup_Create-23704379713 ns/op
BenchmarkPushGroup_Create-43672874840 ns/op
PASS
okgit.shuodev.com/server/msg-dispatcher/internal/dao8.253s

咱们发现,添加cpu超过2个时,并发才能并没有上去,能够得出一个初步定论:Create 函数依赖mysql,它的qps才能在300-400左右。

总结

本文介绍了2种首要的benchmark测验:

  • 演示了怎么运用 b.N 测验函数耗时。而且以斐波拉契算法为例,演示了运用 b.Run 运转多个子测验了以验证不同输入状况下函数的耗时比照,从而把算法复杂度从 O(2^n) 降低到了 O(n)。而且给出了第40个数字比照,2种算法耗时相差3000万倍
  • 演示了怎么运用 b.RunParallel 测验服务QPS,以向mysql刺进群组为例,演示了单机场景下的一种压测方式。

同时,罗列了以下常用的指令以做备忘(官网完好指令):

  • -run regex:运转单测匹配正则的一切单测
  • -bench regex:运转匹配正则的一切benchmark测验
  • -benchtime: 测验履行的时长,默许1s
  • -count:履行次序
  • -cpu 1,2,4:指定运转测验的cpu,以逗号离隔时,会履行屡次。
  • -benchmem: 输出内存分配状况
  • -cpuprofile:输出pprof文件,能够运用go tool pprof 翻开剖析具体cpu耗时。
  • -memprofile:输出pprof文件,能够运用go tool pprof 翻开剖析具体内存分配状况。

参考

  • benchmark 基准测验:geektutu.com/post/hpg-be…
  • How to write benchmarks in Go:dave.cheney.net/2013/06/30/…
  • Introduction to benchmarks in Go:dev.to/mcaci/intro…
  • Testing flags:pkg.go.dev/cmd/go
  • Benchmarking in Golang: Improving function performance:blog.logrocket.com/benchmarkin…
  • 《Go言语标准库》The Golang Standard Library by Example:books.studygolang.com/The-Golang-…

假如觉得写的还不错,欢迎订阅公众号:《Go和分布式IM》,一周一篇Go实战文章,及时推送不遗漏。