Benchmarks

Go 单元测试中,只是验证了算法的正确性,对于算法的复杂度只是从一次结果判断,实际上,如果考虑多次的算法时间复杂度,看看算法的性能就需要使用Benchmarks。

Benchmarks的使用仍然需要在_test.go文件中,并且开头是Benchmark,形参必须是b *testing.B。

我们接着对Go 单元测试的例子中,Search_test.go文件添加如下函数

Search_test.go

func BenchmarkBinarySearch1(b *testing.B) {
	arr:=make([]int,0)
	for i:=0;i<100000;i++{
		arr=append(arr,i)
	}
	for i:=0;i<b.N;i++{
		BinarySearch1(arr,rand.Int()%100000)
	}
}
func BenchmarkBinarySearch2(b *testing.B) {
	arr:=make([]int,0)
	for i:=0;i<100000;i++{
		arr=append(arr,i)
	}
	for i:=0;i<b.N;i++{
		BinarySearch2(arr,rand.Int()%100000)
	}
}

运行时,需要加上-bench

基准函数会运行目标代码 b.N 次。在基准执行期间,会调整 b.N 直到基准测试函数持续足够长的时间。表明b.N是不确定的值,这是为了准确得到算法的性能。

对于足够长的时间,下面是指1s以上

b.N 从 1 开始,如果基准函数在 1 秒内就执行完了,那么 b.N 的值会递增以便基准函数再重新执行,即基准函数默认要运行 1 秒,如果该函数的执行时间在 1 秒内就运行完了,那么就递增 b.N 的值,重新再执行一次

命令

go test -bench=. -run=none

输出

goos: windows
goarch: amd64
pkg: awesomeProject/Search
cpu: Intel(R) Core(TM) i7-8550U CPU @ 1.80GHz
BenchmarkBinarySearch1-8           45808             34748 ns/op
BenchmarkBinarySearch2-8              92          14177941 ns/op
PASS
ok      awesomeProject/Search   4.278s

解释

参数介绍

-bench=. :表示的是运行所有的基准测试,. 表示全部。

-run=none:表示过滤掉单元测试,不去跑UT的cases。

输出的结果内容分析:goos: windows:表示的是操作系统是windows。

goarch: amd64:表示目标平台的体系架构是amd64。

BenchmarkBinarySearch1-8:BenchmarkBinarySearch1表示运行的函数名称; 8表示的是,运行时对应的GOMAXPROCS的值,此数字默认为启动时 Go 进程可见的 CPU 数。。

45808 :表示的是b.N的在1s以上的值,循环执行了 45808 次。

34748ns/op:表示执行一次这个函数,消耗的时间是34748ns。

一些标志的解释

  1. -cpu可以定制cpu的数量

命令

go test -bench=. -cpu=1,2,4  -run=none

输出

goos: windows
goarch: amd64
pkg: awesomeProject/Search
cpu: Intel(R) Core(TM) i7-8550U CPU @ 1.80GHz
BenchmarkBinarySearch1             77134             16858 ns/op
BenchmarkBinarySearch1-2           67426             17360 ns/op
BenchmarkBinarySearch1-4           69294             16515 ns/op
BenchmarkBinarySearch2               247           5035322 ns/op
BenchmarkBinarySearch2-2             240           4969318 ns/op
BenchmarkBinarySearch2-4             235           5096158 ns/op
PASS
ok      awesomeProject/Search   9.280s

不同CPU差别不大,因为没有涉及到并行吧

  1. -benchtime改变运行持续的时间,默认是大于1s,现在是大于10s

命令

go test -bench=. -benchtime=10s -run=none

-benchtime=10s:表示的是运行时间为10s,默认的时间是1s。

输出

goos: windows
goarch: amd64
pkg: awesomeProject/Search
cpu: Intel(R) Core(TM) i7-8550U CPU @ 1.80GHz
BenchmarkBinarySearch1-8          775342             18227 ns/op
BenchmarkBinarySearch2-8            1404           7309653 ns/op
PASS
ok      awesomeProject/Search   25.515s

  1. 通过-count 标志,可以指定基准测试多跑几次

命令

go test -bench=. -benchtime=2s -count=5 -run=none

输出

C:\Users\WinterStar\go\src\awesomeProject\Search>go test -bench=. -benchtime=2s -count=5 -run=none
goos: windows
goarch: amd64
pkg: awesomeProject/Search
cpu: Intel(R) Core(TM) i7-8550U CPU @ 1.80GHz
BenchmarkBinarySearch1-8          160483             15417 ns/op
BenchmarkBinarySearch1-8          155282             14992 ns/op
BenchmarkBinarySearch1-8          149983             15191 ns/op
BenchmarkBinarySearch1-8          156448             14955 ns/op
BenchmarkBinarySearch1-8          161452             15440 ns/op
BenchmarkBinarySearch2-8             478           5082332 ns/op
BenchmarkBinarySearch2-8             492           4918752 ns/op
BenchmarkBinarySearch2-8             472           5147550 ns/op
BenchmarkBinarySearch2-8             459           5138692 ns/op
BenchmarkBinarySearch2-8             459           5126518 ns/op
PASS
ok      awesomeProject/Search   27.379s

耗时管理

在启动或者循环过程中,存在耗时可能会影响函数的性能测试b.ResetTimer() 函数可以用于忽略启动的累积耗时

如果在每次循环迭代中,你有一些费时的配置逻辑,要使用 b.StopTimer() 和 b.StartTimer() 函数来暂定基准测试计时器。

func BenchmarkBinarySearch1(b *testing.B) {
	arr:=make([]int,0)
	for i:=0;i<100000;i++{
		arr=append(arr,i)
	}
	b.ResetTimer()
	for i:=0;i<b.N;i++{
		b.StopTimer()
		val:=rand.Int()%100000
		b.StartTimer()
		BinarySearch1(arr,val)
	}
}
func BenchmarkBinarySearch2(b *testing.B) {
	arr:=make([]int,0)
	for i:=0;i<100000;i++{
		arr=append(arr,i)
	}
	b.ResetTimer()
	for i:=0;i<b.N;i++{
		b.StopTimer()
		val:=rand.Int()%100000
		b.StartTimer()
		BinarySearch2(arr,val)
	}
}

命令

go test -bench=. -run=none

输出

goos: windows
goarch: amd64
pkg: awesomeProject/Search
cpu: Intel(R) Core(TM) i7-8550U CPU @ 1.80GHz
BenchmarkBinarySearch1-8           66144             17916 ns/op
BenchmarkBinarySearch2-8             226           4947265 ns/op
PASS
ok      awesomeProject/Search   5.016s

可以看见时间变短了。

内存分配

内存分配的次数和分配的大小和基准测试的执行时间强相关。你可以通过在代码中增加 b.ReportAllocs() 函数来告诉 testing 框架记录内存分配的数据。

func BenchmarkBinarySearch1(b *testing.B) {
	b.ReportAllocs()
	arr:=make([]int,0)
	for i:=0;i<100000;i++{
		arr=append(arr,i)
	}
	b.ResetTimer()
	for i:=0;i<b.N;i++{
		b.StopTimer()
		val:=rand.Int()%100000
		b.StartTimer()
		BinarySearch1(arr,val)
	}
}
func BenchmarkBinarySearch2(b *testing.B) {
	b.ReportAllocs()
	arr:=make([]int,0)
	for i:=0;i<100000;i++{
		arr=append(arr,i)
	}
	b.ResetTimer()
	for i:=0;i<b.N;i++{
		b.StopTimer()
		val:=rand.Int()%100000
		b.StartTimer()
		BinarySearch2(arr,val)
	}
}

命令

go test -bench=. -run=none

输出

goos: windows
goarch: amd64
pkg: awesomeProject/Search
cpu: Intel(R) Core(TM) i7-8550U CPU @ 1.80GHz
BenchmarkBinarySearch1-8           67902             18285 ns/op               0 B/op          0 allocs/op
BenchmarkBinarySearch2-8             248           4713975 ns/op              56 B/op          2 allocs/op
PASS
ok      awesomeProject/Search   5.098s

你也可以使用 go test -benchmem 标识来强制 testing 框架打印出所有基准测试的内存分配次数

-benchmem:表示显示memory的指标。

命令

go test -bench=. -benchmem -run=none

输出

goos: windows
goarch: amd64
pkg: awesomeProject/Search
cpu: Intel(R) Core(TM) i7-8550U CPU @ 1.80GHz
BenchmarkBinarySearch1-8           50349             23428 ns/op               0 B/op          0 allocs/op
BenchmarkBinarySearch2-8             170           6705078 ns/op              56 B/op          2 allocs/op
PASS
ok      awesomeProject/Search   5.316s

0 B/op:表示每次执行操作,分配0B的内存。

0 allocs/op:表示执行一次这个函数,分配内存的次数为1次。

收集基准测试数据

该 testing 包内置了对生成 CPU,内存和模块配置文件的支持。

  • -cpuprofile=$FILE 收集 CPU 性能分析到 $FILE 文件
  • -memprofile=$FILE,将内存性能分析写入到 $FILE 文件,-memprofilerate=N 调节采样频率为 1/N
  • -blockprofile=$FILE,输出内部 goroutine 阻塞的性能分析文件数据到 $FILE

命令

go test -bench=. -benchmem -cpuprofile=c.p -run=none

上述命令会在当前目录下生成c.p的文件

读取文件命令

go tool pprof c.p

使用上述命令可以读取到c.p的内容,并使用protobuf交互

交互命令

top命令可以查看cpu使用情况的前几名

(pprof) top
Showing nodes accounting for 2440ms, 87.14% of 2800ms total
Dropped 45 nodes (cum <= 14ms)
Showing top 10 nodes out of 58
      flat  flat%   sum%        cum   cum%
     880ms 31.43% 31.43%      880ms 31.43%  awesomeProject/Search.BinarySearch1
     720ms 25.71% 57.14%      720ms 25.71%  awesomeProject/Search.BinarySearch2.func1
     400ms 14.29% 71.43%     1080ms 38.57%  sort.doPivot_func
     180ms  6.43% 77.86%      180ms  6.43%  runtime.stdcall1
      90ms  3.21% 81.07%       90ms  3.21%  runtime.stdcall6
      40ms  1.43% 82.50%       40ms  1.43%  runtime.(*mcache).releaseAll
      40ms  1.43% 83.93%      140ms  5.00%  sort.insertionSort_func
      30ms  1.07% 85.00%       30ms  1.07%  internal/reflectlite.Swapper.func5
      30ms  1.07% 86.07%      140ms  5.00%  runtime.readmemstats_m
      30ms  1.07% 87.14%       30ms  1.07%  runtime.updateTimerPMask

解释

最后一列为函数名称,其他各项内容意义如下(转载):

flat:当前函数占用CPU的耗时

flat%:当前函数占用CPU的耗时百分比

sum%:函数占用CPU的累积耗时百分比

cum:当前函数+调用当前函数的占用CPU总耗时

cum%: 当前函数+调用当前函数的占用CPU总耗时百分比

list 函数名 可以查看函数的cpu使用情况

(pprof) list List
Total: 2.80s

可以list后面跟具体的函数

(pprof) list awesomeProject/Search.BinarySearch1
Total: 2.80s
ROUTINE ======================== awesomeProject/Search.BinarySearch1 in C:\Users\WinterStar\go\src\awesomeProject\Search\Search.go
     880ms      880ms (flat, cum) 31.43% of Total
         .          .      1:package Search
         .          .      2:
         .          .      3:import "sort"
         .          .      4:
         .          .      5:func BinarySearch1(arr []int,val int) bool{
     430ms      430ms      6:   for i:=0;i<len(arr);i++{
     450ms      450ms      7:           if arr[i]==val{
         .          .      8:                   return true
         .          .      9:           }
         .          .     10:   }
         .          .     11:   return false
         .          .     12:}

输入web,会打开本地浏览器查看pprof的svg图来分析

Snipaste_2021-08-14_21-35-27.png

注意:

这个web命令需要在生成的机器上安装graphviz

注意编译器的优化

可以看看这篇博客https://blog.csdn.net/qq_28119741/article/details/117935237

上面很多内容都参考该博客,非常感谢