概述
在 Go 语言的自带测试集中,就默认地提供了 Benchmark 的工具,本文介绍的就是这个 Benchmark 工具,包括它的测试原理以及一些相对复杂的测试实现。
简单的 benchmark
一个简单的 benchmark 真的就非常简单,例如:
[root@liqiang.io]# cat benchmark_test.go
func BenchmarkTest(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = add(1, 2)
}
}
func add(a, b int) int {
return a + b
}
然后运行一下看看:
[root@liqiang.io]# go test -test.bench ^BenchmarkTest$ -test.run ^$ .
goos: darwin
goarch: amd64
pkg: xxxxxx
cpu: xxxxxx
BenchmarkTest-12 1000000000 0.2499 ns/op
PASS
ok xxxxxx 0.402s
可以看到,这里返回了一行:
BenchmarkTest-12 1000000000 0.2499 ns/op
第一列就是表示 benchmark 的是哪个函数,第二列表示运行了这个函数多少次(b.N 中的 N),然后最后一个就是 benchmark 的结果,这里每次调用的时间是 0.2499 ns,非常地快。那么可以看到其他 benchmark 吗?例如时间可能因 cpu 的不同或者机器任务的繁忙程度有所不同;此外,很多时候,我们看一段代码速度快不快是一方面,内存占用高不高也是一个很关键的因素,还有 IO 是否频繁也是需要考虑的点(当然,IO 可能反映在速度上),所以这里我们可以加上一个内存相关的分析,我稍微修改一下代码,然后看结果就好了,注意,这里的运行参数我加多了一个:-benchmem
[root@liqiang.io]# go test -test.bench ^BenchmarkTest$ -test.run ^$ . -benchmem
goos: darwin
goarch: amd64
pkg: xxxxxx
cpu: xxxxxx
BenchmarkTest-12 100000000 10.30 ns/op 3 B/op 1 allocs/op
PASS
ok xxxxxx 2.034s
因为我修改了代码,所以时间我们就忽略了,可以发现,在结果中多两列,分别是每次操作(for 里面调用的函数)分配了多少内存,以及每次操作申请了多少次内存。
自定义报告指标
内置的指标就这么几个,如果你觉得还不够,没问题,你可以自己添加指标,例如你想 benchmark 一下自带的排序算法的复杂度是多少,那么就是看在排序的时候比较了多少次,那么可以这么看:
[root@liqiang.io]# cat sort_slice_test.go
func BenchmarkTest(b *testing.B) {
compare := 0
for i := 0; i < b.N; i++ {
sortSlice := []int{1, 2, 3, 4, 5}
sort.Slice(sortSlice, func(i, j int) bool {
compare++
return sortSlice[i] < sortSlice[j]
})
}
b.ReportMetric(float64(compare)/float64(b.N), "compare/op")
}
然后运行一下看看结果:
[root@liqiang.io]# go test -test.bench ^BenchmarkTest$ -test.run ^$ . -benchmem
goos: darwin
goarch: amd64
pkg: xxxxxxx
cpu: xxxxxxx
BenchmarkTest-12 8820426 123.3 ns/op 4.000 compare/op 104 B/op 3 allocs/op
PASS
ok xxxxxxx 2.139s
横向比较
很多时候,我们做 benchmark 其实都是用于比较,例如我们自己写了一个 SDK,然后想和其他相同类型的 SDK 做个性能对比,那么繁琐一点我们可以手动运行多次,每次运行一个 SDK 的 benchmark,然后比较这多次运行的结果;对于我来说,我更想的是,一次运行多个 SDK 的 benchmark,然后直接看多个 SDK 的类似操作的结果,那么可以这么做:
[root@liqiang.io]# cat multi_bench_test.go
func BenchmarkSDK1(b *testing.B) {
for i := 0; i < b.N; i++ {
SDK1.func()
}
}
func BenchmarkSDK2(b *testing.B) {
for i := 0; i < b.N; i++ {
SDK2.func()
}
}
看上去是可行的,但是关系不是那么强,我更希望是类似这样的操作:
[root@liqiang.io]# cat multi_bench_test.go
func BenchmarkFunc(b *testing.B) {
for i := 0; i < b.N; i++ {
SDK1.func()
}
for i := 0; i < b.N; i++ {
SDK2.func()
}
}
但是,如果你直接这么写,你会发现结果不是你想要的,我这里给个例子:
[root@liqiang.io]# cat multi_bench_test.go
func BenchmarkAdd1(b *testing.B) {
for i := 0; i < b.N; i++ {
add(3, 5)
}
}
func BenchmarkAdd2(b *testing.B) {
for i := 0; i < b.N; i++ {
add2(3, 5)
}
}
func BenchmarkTwoAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
add2(3, 5)
}
for i := 0; i < b.N; i++ {
add(3, 5)
}
}
运行一下,结果是这样的,其实就是将两个结果加起来了:
[root@liqiang.io]# go test -test.bench '.*' -test.run ^$ . -benchmem
goos: darwin
goarch: amd64
pkg: xxxxxxxxx
cpu: xxxxxxxxx
BenchmarkAdd1-12 50087434 21.98 ns/op 10 B/op 2 allocs/op
BenchmarkAdd2-12 100000000 10.84 ns/op 5 B/op 1 allocs/op
BenchmarkTwoAdd-12 36373838 32.60 ns/op 16 B/op 3 allocs/op
PASS
ok xxxxxxxxx 3.579s
正确的写法
正确的写法是这样的:
[root@liqiang.io]# cat multi_bench_test.go
func BenchmarkTwoAdd(b *testing.B) {
var funcs = []func(a, b int) int{add, add2}
for idx, f := range funcs {
b.Run("function "+strconv.Itoa(idx), func(b *testing.B) {
for i := 0; i < b.N; i++ {
f(3, 5)
}
})
}
}
然后我们运行一下看结果:
[root@liqiang.io]# go test -test.bench '^BenchmarkTwoAdd$' -test.run ^$ . -benchmem
goos: darwin
goarch: amd64
pkg: xxxxxxxx
cpu: xxxxxxxx
BenchmarkTwoAdd/function_0-12 44406841 24.75 ns/op 10 B/op 2 allocs/op
BenchmarkTwoAdd/function_1-12 90792690 12.98 ns/op 5 B/op 1 allocs/op
PASS
ok xxxxxxxx 3.325s
可以发现这就不是两个函数调用相加了,而是独立的两个函数,但是可以发现,这里的运行时间比单独运行稍微多了一些,但是无伤大雅,我们关注的是横向对比,这里就很明确了。
小结
OK,这就是关于 Go 自带的 Benchmark 的一些介绍和总结了,希望对你有所帮助。