引言
本文偏手册性质,需求写 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/):
高档指令
完好指令参加官方文档: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实战文章,及时推送不遗漏。