benchmark基准测试
# 简单示例
fib.go
package main func fib(n int) int { if n == 0 || n == 1 { return n } return fib(n-2) + fib(n-1) }
1
2
3
4
5
6
7
8fib_test.go
// fib_test.go package main import "testing" func BenchmarkFib(b *testing.B) { for n := 0; n < b.N; n++ { fib(30) // run fib(30) b.N times } }
1
2
3
4
5
6
7
8
9
10benchmark 和普通的单元测试用例一样,都位于
_test.go
文件中。函数名以
Benchmark
开头,参数是b *testing.B
。和普通的单元测试用例很像,单元测试函数名以Test
开头,参数是t *testing.T
。运行
go test -bench='Fib$'
# benchmark 是如何工作的
benchmark 用例的参数
b *testing.B
,有个属性b.N
表示这个用例需要运行的次数。b.N
对于每个用例都是不一样的。那这个值是如何决定的呢?
b.N
从 1 开始,如果该用例能够在 1s 内完成,b.N
的值便会增加,再次执行。b.N
的值大概以 1, 2, 3, 5, 10, 20, 30, 50, 100 这样的序列递增,越到后面,增加得越快。我们仔细观察上述例子的输出:BenchmarkFib-8 202 5980669 ns/op
1BenchmarkFib-8 中的
-8
即GOMAXPROCS
,默认等于 CPU 核数。可以通过-cpu
参数改变GOMAXPROCS
,-cpu
支持传入一个列表作为参数,例如:$ go test -bench='Fib$' -cpu=2,4 . goos: darwin goarch: amd64 pkg: example BenchmarkFib-2 206 5774888 ns/op BenchmarkFib-4 205 5799426 ns/op PASS ok example 3.563s
1
2
3
4
5
6
7
8在这个例子中,改变 CPU 的核数对结果几乎没有影响,因为这个 Fib 的调用是串行的。
202
和5980669 ns/op
表示用例执行了 202 次,每次花费约 0.006s。总耗时比 1s 略多。
# 提升准确度
对于性能测试来说,提升测试准确度的一个重要手段就是增加测试的次数。我们可以使用
-benchtime
和-count
两个参数达到这个目的。benchmark 的默认时间是 1s,那么我们可以使用
-benchtime
指定为 5s。例如:$ go test -bench='Fib$' -benchtime=5s . goos: darwin goarch: amd64 pkg: example BenchmarkFib-8 1033 5769818 ns/op PASS ok example 6.554s
1
2
3
4
5
6
7实际执行的时间是 6.5s,比 benchtime 的 5s 要长,测试用例编译、执行、销毁等是需要时间的。
将
-benchtime
设置为 5s,用例执行次数也变成了原来的 5倍,每次函数调用时间仍为 0.6s,几乎没有变化。-benchtime
的值除了是时间外,还可以是具体的次数。例如,执行 30 次可以用-benchtime=30x
:$ go test -bench='Fib$' -benchtime=50x . goos: darwin goarch: amd64 pkg: example BenchmarkFib-8 50 6121066 ns/op PASS ok example 0.319s
1
2
3
4
5
6
7调用 50 次
fib(30)
,仅花费了 0.319s。-count
参数可以用来设置 benchmark 的轮数。例如,进行 3 轮 benchmark。$ go test -bench='Fib$' -benchtime=5s -count=3 . goos: darwin goarch: amd64 pkg: example BenchmarkFib-8 975 5946624 ns/op BenchmarkFib-8 1023 5820582 ns/op BenchmarkFib-8 961 6096816 ns/op PASS ok example 19.463s
1
2
3
4
5
6
7
8
9
# 内存分配情况
-benchmem
参数可以度量内存分配的次数。内存分配次数也性能也是息息相关的,例如不合理的切片容量,将导致内存重新分配,带来不必要的开销。在下面的例子中,
generateWithCap
和generate
的作用是一致的,生成一组长度为 n 的随机序列。唯一的不同在于,generateWithCap
创建切片时,将切片的容量(capacity)设置为 n,这样切片就会一次性申请 n 个整数所需的内存。// generate_test.go package main import ( "math/rand" "testing" "time" ) func generateWithCap(n int) []int { rand.Seed(time.Now().UnixNano()) nums := make([]int, 0, n) for i := 0; i < n; i++ { nums = append(nums, rand.Int()) } return nums } func generate(n int) []int { rand.Seed(time.Now().UnixNano()) nums := make([]int, 0) for i := 0; i < n; i++ { nums = append(nums, rand.Int()) } return nums } func BenchmarkGenerateWithCap(b *testing.B) { for n := 0; n < b.N; n++ { generateWithCap(1000000) } } func BenchmarkGenerate(b *testing.B) { for n := 0; n < b.N; n++ { generate(1000000) } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38运行该用例的结果是:
$ go test -bench='Generate' . goos: darwin goarch: amd64 pkg: example BenchmarkGenerateWithCap-8 44 24294582 ns/op BenchmarkGenerate-8 34 30342763 ns/op PASS ok example 2.171s
1
2
3
4
5
6
7
8可以看到生成 100w 个数字的随机序列,
GenerateWithCap
的耗时比Generate
少 20%。我们可以使用
-benchmem
参数看到内存分配的情况:goos: darwin goarch: amd64 pkg: example BenchmarkGenerateWithCap-8 43 24335658 ns/op 8003641 B/op 1 allocs/op BenchmarkGenerate-8 33 30403687 ns/op 45188395 B/op 40 allocs/op PASS ok example 2.121s
1
2
3
4
5
6
7Generate
分配的内存是GenerateWithCap
的 6 倍,设置了切片容量,内存只分配一次,而不设置切片容量,内存分配了 40 次。
# 测试不同的输入
不同的函数复杂度不同,O(1),O(n),O(n^2) 等,利用 benchmark 验证复杂度一个简单的方式,是构造不同的输入。对刚才的 benchmark 稍作改造,便能够达到目的。
// generate_test.go package main import ( "math/rand" "testing" "time" ) func generate(n int) []int { rand.Seed(time.Now().UnixNano()) nums := make([]int, 0) for i := 0; i < n; i++ { nums = append(nums, rand.Int()) } return nums } func benchmarkGenerate(i int, b *testing.B) { for n := 0; n < b.N; n++ { generate(i) } } func BenchmarkGenerate1000(b *testing.B) { benchmarkGenerate(1000, b) } func BenchmarkGenerate10000(b *testing.B) { benchmarkGenerate(10000, b) } func BenchmarkGenerate100000(b *testing.B) { benchmarkGenerate(100000, b) } func BenchmarkGenerate1000000(b *testing.B) { benchmarkGenerate(1000000, b) }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27这里,我们实现一个辅助函数
benchmarkGenerate
允许传入参数 i,并构造了 4 个不同输入的 benchmark 用例。运行结果如下:$ go test -bench . goos: darwin goarch: amd64 pkg: example BenchmarkGenerate1000-8 34048 34643 ns/op BenchmarkGenerate10000-8 4070 295642 ns/op BenchmarkGenerate100000-8 403 3230415 ns/op BenchmarkGenerate1000000-8 39 32083701 ns/op PASS ok example 6.597s
1
2
3
4
5
6
7
8
9
10通过测试结果可以发现,输入变为原来的 10 倍,函数每次调用的时长也差不多是原来的 10 倍,这说明复杂度是线性的。