go benchmark 默认测试时间是 1s,同样的原理,为了提升测试准确度,我们可以使用该参数适当增加时长。
➜ learnGolang go test -bench='Fib10$'
goos: linux
goarch: amd64
pkg: learnGolang
BenchmarkFib10-12 4153650 288 ns/op
PASS
ok learnGolang 1.491s
# 指定时长为 5s
➜ learnGolang go test -bench='Fib10$' -benchtime=5s
goos: linux
goarch: amd64
pkg: learnGolang
BenchmarkFib10-12 20616992 288 ns/op
PASS
ok learnGolang 6.235s
还是高中物理学,我们也可以指定物理移动的距离,然后测量所耗费的时间,计算平均速度。
该参数还支持特殊的形式 Nx ,用来指定被测程序的运行次数。
# 指定运行次数为 1000 次
➜ learnGolang go test -bench='Fib10$' -benchtime=1000x
goos: linux
goarch: amd64
pkg: learnGolang
BenchmarkFib10-12 1000 305 ns/op
PASS
ok learnGolang 0.002s
-count n
同样类似与测量物体速度,为了提升精确度,我们多做几次测试。
➜ learnGolang go test -bench='Fib10$' -benchtime=5s -count=3
goos: linux
goarch: amd64
pkg: learnGolang
BenchmarkFib10-12 19596388 288 ns/op
BenchmarkFib10-12 20796957 290 ns/op
BenchmarkFib10-12 20492478 291 ns/op
PASS
ok learnGolang 18.542s
-cpu n
该参数可以设置 benchmark 所使用的 CPU 核数。
下面我们模拟一次多核并行计算的例子,并观察设置不同核数后的测试结果
// main.go
func parallelExam() int {
chs := make([]chan int, 10) // 设置 10 个协程去并行计算
for i := 0; i < len(chs); i++ {
chs[i] = make(chan int, 1)
go parallelSum(chs[i])
}
sum := 0
for _, ch := range chs {
res := <-ch
sum += res
}
return sum
}
func parallelSum(ch chan int) {
defer close(ch)
sum := 0
for i := 1; i <= 100000; i++ { // 10万
sum += i
}
ch <- sum
}
// main_test.go
func BenchmarkParallelExam(b *testing.B) {
for n := 0; n < b.N; n++ {
parallelExam()
}
}
➜ learnGolang go test -bench='BenchmarkParallelExam' -cpu=1,4,6,10,12
goos: linux
goarch: amd64
pkg: learnGolang
BenchmarkParallelExam 3154 366754 ns/op
BenchmarkParallelExam-4 9316 119747 ns/op
BenchmarkParallelExam-6 10000 107040 ns/op
BenchmarkParallelExam-10 10000 108144 ns/op
BenchmarkParallelExam-12 9891 110018 ns/op
PASS
ok learnGolang 5.604s
从运行结果看出,随着 CPU 核数的增加,性能逐步提升,但是到一定阈值后,性能趋于稳定,此时再增加 CPU 核数,性能反而下降,因为 CPU 核心之间的切换也是需要成本的。
-benchmem
除了速度,内存分配情况也是需要我们重点关注的指标。
go 语言中,slice 有一个 cap 属性,合理的设置该值,可以减少内存分配次数,分配大小,提升程序性能。
// main.go
func sliceNoCap() {
s := make([]int, 0) // 未设置 cap 值
for i := 0; i < 10000; i++ {
s = append(s, i)
}
}
func sliceWithCap() {
s := make([]int, 0, 10000) // 预先设置 cap 值
for i := 0; i < 10000; i++ {
s = append(s, i)
}
}
// main_test.go
func BenchmarkSliceNoCap(b *testing.B) {
for n := 0; n < b.N; n++ {
sliceNoCap()
}
}
func BenchmarkSliceWithCap(b *testing.B) {
for n := 0; n < b.N; n++ {
sliceWithCap()
}
}
➜ learnGolang go test -bench='Cap$' -benchmem .
goos: linux
goarch: amd64
pkg: learnGolang
BenchmarkSliceNoCap-12 31318 38614 ns/op 386297 B/op 20 allocs/op
BenchmarkSliceWithCap-12 111764 10269 ns/op 81920 B/op 1 allocs/op
PASS
ok learnGolang 2.858s
If a benchmark needs some expensive setup before running, the timer may be reset
如果在整个 benchmark 执行前,需要一些耗时的准备工作,我们需要将这部分耗时忽略掉
func BenchmarkFib(b *testing.B) {
time.Sleep(3 * time.Second) // 模拟耗时的准备工作
b.ResetTimer() // 重置计时器,忽略前面的准备时间
for n := 0; n < b.N; n++ {
fib(10)
}
}
StopTimer & StartTimer
StopTimer stops timing a test. This can be used to pause the timer while performing complex initialization that you don't want to measure.
StartTimer starts timing a test. This function is called automatically before a benchmark starts, but it can also be used to resume timing after a call to StopTimer.
前言
基准测试(benchmark)是 go testing 库提供的,用来度量程序性能,算法优劣的利器。
在日常生活中,我们使用速度 m/s(单位时间内物体移动的距离)大小来衡量一辆跑车的性能,同理,我们可以使用”单位时间内程序运行的次数“来衡量程序的性能。
在日常开发中,如果和同事在代码实现上有分歧,不用多费口舌,跑个分就知道谁牛X。
注意:在进行基准测试时,硬件资源直接影响测试结果,为了保证测试结果的可重复性,需要尽可能地保证硬件资源一致。(单一变量原则)
快速开始
创建项目
learnGolang
创建文件
main.go
,编写我们的被测函数创建文件
main_test.go
,编写基准测试用例package
内的测试文件以_test.go
结尾,其中的测试用例格式为func BenchmarkXxx(b *testing.B)
,注意Xxx
首字母要大写(即驼峰命名法)开始运行
go test [packages]
指定测试范围go test
命令默认不执行 benchmark 测试,需要加上-bench
参数,该参数支持正则表达式,只有匹配到的测试用例才会执行,使用.
则运行所有测试用例GOMAXPROCS
,默认等于 CPU 核数3287449 357 ns/op
表示单位时间内(默认是1s)被测函数运行了 3287449 次,每次运行耗时 357ns,3287449*357ns=1.173s(耗时比 1s 略多,因为测试用例执行、销毁等是需要时间的)
ok learnGolang 3.418s
表示本次测试总耗时-benchtime t
在高中物理学中,由于测试物体瞬时速度不好实现,我们可以让物体多移动一段时间,然后采用“总距离/时间段”算出平均速度来代替瞬时速度。
go benchmark 默认测试时间是 1s,同样的原理,为了提升测试准确度,我们可以使用该参数适当增加时长。
还是高中物理学,我们也可以指定物理移动的距离,然后测量所耗费的时间,计算平均速度。
该参数还支持特殊的形式
Nx
,用来指定被测程序的运行次数。-count n
同样类似与测量物体速度,为了提升精确度,我们多做几次测试。
-cpu n
该参数可以设置 benchmark 所使用的 CPU 核数。
下面我们模拟一次多核并行计算的例子,并观察设置不同核数后的测试结果
从运行结果看出,随着 CPU 核数的增加,性能逐步提升,但是到一定阈值后,性能趋于稳定,此时再增加 CPU 核数,性能反而下降,因为 CPU 核心之间的切换也是需要成本的。
-benchmem
除了速度,内存分配情况也是需要我们重点关注的指标。
go 语言中,
slice
有一个cap
属性,合理的设置该值,可以减少内存分配次数,分配大小,提升程序性能。可以看到前者每次执行会分配 386297 字节的内存,约等于后者的 3.76 倍,每次执行会分配内存 20 次,是后者的 20 倍。
ResetTimer
如果在整个 benchmark 执行前,需要一些耗时的准备工作,我们需要将这部分耗时忽略掉
StopTimer & StartTimer
如果在被测函数每次执行前,需要一些准备工作,我们可以使用
StopTimer
暂停计时,准备工作完成后,使用StartTimer
继续计时。参考
https://geektutu.com/post/hpg-benchmark.html
https://golang.org/pkg/testing/
https://golang.org/cmd/go/#hdr-Testing_flags
https://dave.cheney.net/high-performance-go-workshop/gophercon-2019.html