基准测试(benchmark)是 go testing 库提供的,用来度量程序性能,算法优劣的利器。html
在平常生活中,咱们使用速度 m/s(单位时间内物体移动的距离)大小来衡量一辆跑车的性能,同理,咱们可使用”单位时间内程序运行的次数“来衡量程序的性能。linux
在平常开发中,若是和同事在代码实现上有分歧,不用多费口舌,跑个分就知道谁牛X。golang
注意:在进行基准测试时,硬件资源直接影响测试结果,为了保证测试结果的可重复性,须要尽量地保证硬件资源一致。(单一变量原则)正则表达式
建立项目 learnGolang
算法
mkdir learnGolang cd learnGolang go mod init learnGolang
建立文件 main.go
,编写咱们的被测函数shell
package main // 斐波那契数列 func fib(n int) int { if n < 2 { return n } return fib(n-1) + fib(n-2) } func sum(a, b int) int { return a + b }
建立文件 main_test.go
,编写基准测试用例编程
package main import "testing" func BenchmarkFib10(b *testing.B) { for n := 0; n < b.N; n++ { fib(10) } } func BenchmarkFib20(b *testing.B) { for n := 0; n < b.N; n++ { fib(20) } } func BenchmarkSum(b *testing.B) { for n := 0; n < b.N; n++ { sum(1, 2) } }
package
内的测试文件以 _test.go
结尾,其中的测试用例格式为 func BenchmarkXxx(b *testing.B)
,注意 Xxx
首字母要大写(即驼峰命名法)开始运行windows
$ go test -bench=. . goos: windows goarch: amd64 pkg: learnGolang BenchmarkFib10-4 3360627 362 ns/op BenchmarkFib20-4 26676 44453 ns/op BenchmarkSum-4 1000000000 0.296 ns/op PASS ok learnGolang 3.777s
go test [packages]
指定测试范围方法一 | 方法二 | |
---|---|---|
运行当前 package 内的用例 | go test packageName | go test . |
运行子 package 内的用例 | go test packageName/subName | go test ./subName |
递归运行全部的用例 | go test packageName/... | go test ./... |
go test
命令默认不执行 benchmark 测试,须要加上 -bench
参数,该参数支持正则表达式,只有匹配到的测试用例才会执行,使用 .
则运行全部测试用例# 只运行斐波那契数列测试用例 $ go test -bench='.*Fib.*' . goos: windows goarch: amd64 pkg: learnGolang BenchmarkFib10-4 3287449 357 ns/op BenchmarkFib20-4 27097 44461 ns/op PASS ok learnGolang 3.418s
GOMAXPROCS
,默认等于 CPU 核数3287449 357 ns/op
表示单位时间内(默认是1s)被测函数运行了 3287449 次,每次运行耗时 357ns,app
3287449*357ns=1.173s(耗时比 1s 略多,由于测试用例执行、销毁等是须要时间的)函数
ok learnGolang 3.418s
表示本次测试总耗时
在高中物理学中,因为测试物体瞬时速度很差实现,咱们可让物体多移动一段时间,而后采用“总距离/时间段”算出平均速度来代替瞬时速度。
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
一样相似与测量物体速度,为了提高精确度,咱们多作几回测试。
➜ 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
该参数能够设置 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 核心之间的切换也是须要成本的。
除了速度,内存分配状况也是须要咱们重点关注的指标。
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
能够看到前者每次执行会分配 386297 字节的内存,约等于后者的 3.76 倍,每次执行会分配内存 20 次,是后者的 20 倍。
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 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.
若是在被测函数每次执行前,须要一些准备工做,咱们可使用 StopTimer
暂停计时,准备工做完成后,使用 StartTimer
继续计时。
func BenchmarkFib(b *testing.B) { for n := 0; n < b.N; n++ { b.StopTimer() // 暂停计时 prepare() // 每次函数执行前的准备工做 b.StartTimer() // 继续计时 funcUnderTest() // 被测函数 } }