前言
go语言本身提供了基准测试的支持,基准测试有以下要求
- 文件以_test.go结尾
- 基准测试函数以Benchmark开头
- 函数参数为*testing.B 类型,无返回值
- 测试代码放到for循环中
b.N 是基准测试框架提供的,表示循环次数,此值由框架多次执行被测函数后确定。
误解
以前一直没有仔细思考benchmark底层具体的实现,想当然的认为是测试框架会统计每次循环耗时,观察是否趋于稳定,待稳定后,得到一个N值。这种想法其实很幼稚,稍微一思考就不太能实现。首先,for循环是在具体的测试函数中,前后有没有任何可供框架插入勾子函数的机会,统计单次循环耗时其实就不可行;其次,代码在for循环中执行b.N在编译执行后肯定是一个具体的值,不具有动态变更的能力,因此只可能是框架重复执行测试函数,b.N的取值按照某个初始值后阶梯式增长,然后统计耗时
验证
代码如下
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
|
func Op1() {
for cnt := 0; cnt < 1000; cnt++ {
}
}
func Op2() {
for cnt := 0; cnt < 1000000; cnt++ {
}
}
//BenchmarkOp1-12 4269636 276 ns/op
func BenchmarkOp1(b *testing.B) {
for cnt := 0; cnt < b.N; cnt++ {
Op1()
}
fmt.Printf("N %v\n", b.N)
}
//BenchmarkOp2-12 4518 254495 ns/op
func BenchmarkOp2(b *testing.B) {
for cnt := 0; cnt < b.N; cnt++ {
Op2()
}
fmt.Printf("N %v\n", b.N)
}
//BenchmarkOP3-12 1 10004286824 ns/op
func BenchmarkOP3(b *testing.B) {
for cnt := 0; cnt < b.N; cnt++ {
Op1()
}
time.Sleep(10 * time.Second)
}
//BenchmarkOP4-12 1 1001792637 ns/op
func BenchmarkOP4(b *testing.B) {
for cnt := 0; cnt < b.N; cnt++ {
Op1()
}
time.Sleep(1 * time.Second)
}
//BenchmarkGoTakeOP1-12 4213207 279 ns/op
func BenchmarkGoTakeOP1(b *testing.B) {
var wg sync.WaitGroup
for cnt := 0; cnt < b.N; cnt++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
Op1()
}(cnt)
}
wg.Wait()
}
//BenchmarkGoTakeOP2-12 25840 49985 ns/op
func BenchmarkGoTakeOP2(b *testing.B) {
var wg sync.WaitGroup
for cnt := 0; cnt < b.N; cnt++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
Op2()
}(cnt)
}
wg.Wait()
}
|
首先分别有两个操作函数op1和op2,benchmark的输出结果字段含义:

观察BenchmarkOp1的输出,如下:
1
2
3
4
5
6
7
|
BenchmarkOp1
N 1
N 100
N 10000
N 1000000
N 4109054
BenchmarkOp1-12 4109054 267 ns/op
|
可以知道N的取值是多次阶梯递增后取得的。此处其实还说明了一个信息,基准测试框架并不会去关注单次循环是否趋于稳定,基准测试框架只是忠诚的评估了在测试时间(默认是1s)内大约能执行多少次循环,换句话说,b.N的值是该测试函数在1s或者超过1s时,总共执行的次数。如果单次循环执行耗时,那么将只会执行一次(观察BenchmarkOP3的测试结果可知)