go 语言 基准测试 结果解读

基准测试

是测量一个程序在固定工作负载下的性能,使用 -bench 标记可以对代码进行基准测试

go test -v -bench=. -run=^$ gott/hello
goos: darwin
goarch: amd64
pkg: gott/hello
BenchmarkHello-4         4964053               228.5 ns/op
PASS
ok      gott/hello      2.386s
  • .表示执行代码包中所有的基准测试用例(前缀为Benchmark的方法),由于功能测试也会被运行,但同时运行功能测试会影响性能测试的结果,通过加入-run=^$,来禁止功能测试与性能测试一起执行。^$意味着:只执行名称为空的功能测试函数,即不执行任何测试函数。

  • 指定运行某个基准测试

    go test -v -bench=BenchmarkHello gott/hello

    功能测试也会一起运行。。。。。。

b.N值的确定

func BenchmarkHello(b *testing.B) {
	for i := 0; i < b.N; i++ {
		hello("Max")
	}
}

当测试开始时,b.N的值被设置为1,执行后如果没有超过默认执行时间上限(默认为1秒),则加大b.N的值,按某种规则一直递增,直到执行时间等于或超过上限,那么就用这一次的b.N的值,做为测试的最终结果

BenchmarkHello-4         4964053               228.5 ns/op
PASS
ok      gott/hello      2.386s
  • BenchmarkHello-4表示执行 BenchmarkHello 时,所用的最大P的数量为4
  • 4964053: 表示hello()方法在达到这个执行次数时,等于或超过了1秒
  • 228.5 ns/op: 表示每次执行hello()所消耗的平均执行时间
  • 2.386s:表示测试总共用时

测试总时间的计算

既然4964053表示1秒或大于1秒时执行的次数,那么测试总时间用时却是2.386s,超出了不少,这是为什么呢

func BenchmarkHello(b *testing.B) {
	for i := 0; i < b.N; i++ {
		hello("Max")
	}
	b.Log("NNNNN:", b.N)
}

在测试中加入b.Log("NNNNN:", b.N),再执行基准测试,并加入-v,打印测试中的日志

go test -v -bench=. -run=^$ gott/hello
=== RUN   TestHello
    hello_test.go:15: hello Max
--- PASS: TestHello (0.00s)
=== RUN   TestPrint
    hello_test.go:19: just Print
--- PASS: TestPrint (0.00s)
goos: darwin
goarch: amd64
pkg: gott/hello
BenchmarkHello
    hello_test.go:26: NNNNN: 1
    hello_test.go:26: NNNNN: 100
    hello_test.go:26: NNNNN: 10000
    hello_test.go:26: NNNNN: 1000000
    hello_test.go:26: NNNNN: 3541896
    hello_test.go:26: NNNNN: 4832275
BenchmarkHello-4         4832275               236.8 ns/op
PASS
ok      gott/hello      2.395s

可以看到b.Log("NNNNN:", b.N)被执行了6次,这证明了之前提到的,测试会对b.N依次递增,直到执行时间等于或超过上限。在对BenchmarkHello()运行基准测试时,N值依次按1,100,10000,1000000,3541896,4832275递增,直到执行次数为4832275时,执行时间等于或超过了上限。

同时也说明BenchmarkHello()一共被调用了6次,每次运行BenchmarkHello()都要消耗一定的时间,所以测试总耗时为这6次调用时间之和,2.395s,超过了1秒

平均执行时间的计算

应该用,运行4832275时所消耗的时间 tt / 4832275 = 236.8 ns/op

  • 如果用 测试总共用时 / 最多可以执行的次数 则不等于 平均执行时间,即 2.386 * (1000 ** 3) / 4832275 = 493.7 大于测试结果中的236.8 ns/op
  • 如果用 1 秒 / 4832275 = 206 ns ,与236.8 ns/op并不是很接近
  • 如果把尝试过程中的运行次数也加入进来total = 1 + 100 + 10000 + 1000000 + 3541896 + 4832275,即2.386 * (1000 ** 3) / 9384272 = 254.25236.8 ns/op接近
  • 反推运行时间:4832275 * 236.8 ns / 1000 ** 3 = 1.14s ,测试结果使用了运行时间超过1秒上限时的数值

问题:是否测试总时间一定会超过1秒?答:因为要找到最大可执行次数,而在这之前肯定要进行多次尝试,所以测试总时间应该总是会超过1秒的。

benchtime 标记

可以通过 -benchtime标记修改默认时间上限,比如改为3秒

go test -v -bench=. -benchtime=3s -run=^$ gott/hello
goos: darwin
goarch: amd64
pkg: gott/hello
BenchmarkHello
    hello_test.go:31: NNNNN: 1
    hello_test.go:32: /Users/ga/m/opt/go/go_root
    hello_test.go:31: NNNNN: 100
    hello_test.go:32: /Users/ga/m/opt/go/go_root
    hello_test.go:31: NNNNN: 10000
    hello_test.go:32: /Users/ga/m/opt/go/go_root
    hello_test.go:31: NNNNN: 1000000
    hello_test.go:32: /Users/ga/m/opt/go/go_root
    hello_test.go:31: NNNNN: 15927812
    hello_test.go:32: /Users/ga/m/opt/go/go_root
BenchmarkHello-4   	15927812	       223.4 ns/op
PASS
ok  	gott/hello	3.802s

还可以设置具体的探索次数最大值,格式为-benchtime=Nx

go test gott/hello -run=^$ -bench=BenchmarkHello -benchtime=50x
goos: darwin
goarch: amd64
pkg: gott/hello
BenchmarkHello-4              50              2183 ns/op
--- BENCH: BenchmarkHello-4
    hello_test.go:35: NNNNN: 1
    hello_test.go:36: /Users/ga/m/opt/go/go_root
    hello_test.go:35: NNNNN: 50
    hello_test.go:36: /Users/ga/m/opt/go/go_root
PASS
ok      gott/hello      0.011s

b.N的值被设置为50,函数运行了50次

benchmem 标记

可以通过-benchmem标记查看内存使用信息

go test gott/hello -run=^$ -bench=BenchmarkHello -benchmem
goos: darwin
goarch: amd64
pkg: gott/hello
BenchmarkHello-4         5137456               223.1 ns/op            32 B/op          2 allocs/op
--- BENCH: BenchmarkHello-4
    hello_test.go:35: NNNNN: 1
    hello_test.go:36: /Users/ga/m/opt/go/go_root
    hello_test.go:35: NNNNN: 100
    hello_test.go:36: /Users/ga/m/opt/go/go_root
    hello_test.go:35: NNNNN: 10000
    hello_test.go:36: /Users/ga/m/opt/go/go_root
    hello_test.go:35: NNNNN: 1000000
    hello_test.go:36: /Users/ga/m/opt/go/go_root
    hello_test.go:35: NNNNN: 5137456
    hello_test.go:36: /Users/ga/m/opt/go/go_root
        ... [output truncated]
PASS
ok      gott/hello      1.399s
  • 32 B/op:平均每次迭代内存分配的字节数
  • 2 allocs/op:平均每次迭代内存分配的次数

平均每次迭代计算的依据应该使用的是 b.N=5137456迭代次数

基准测试的用途

一般用于对比两个不同的操作所消耗的时间,如

  • 渐近增长函数的运行时间

    一个函数需要1ms处理1,000个元素,处理10000或1百万将需要多少时间呢

  • I/O缓存该设置为多大

    基准测试可以帮助我们选择在性能达标情况下所需的最小内存

  • 确定哪种算法更好

比较型的基准测试代码

func benchmark(b *testing.B, size int) { /* ... */ }
func Benchmark10(b *testing.B)         { benchmark(b, 10) }
func Benchmark100(b *testing.B)        { benchmark(b, 100) }
func Benchmark1000(b *testing.B)       { benchmark(b, 1000) }

通过参数size来控制输入的大小,而不是直接修改b.N的值,除非你只是想知道一个固定大小的迭代的耗时,否则基准测试的结果将毫无意义