在日常开发中,基准测试是必不可少的,基准测试次要是通过测试CPU和内存的效率问题,来评估被测试代码的性能,进而找到更好的解决方案。
而Go语言中自带的benchmark则是一件十分神奇的测试利器。有了它,开发者能够方便快捷地在测试一个函数办法在串行或并行环境下的基准体现。指定一个工夫(默认是1秒),看测试对象在达到或超过工夫下限时,最多能被执行多少次和在此期间测试对象内存分配情况。
1 benchmark的常见用法
1.1 如何写一个benchmark的基准测试
import ( "fmt" "testing")func BenchmarkSprint(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { fmt.Sprint(i) }}
对以上代码做如下阐明:
- 基准测试代码文件必须是_test.go结尾,和单元测试一样;
- 基准测试的函数以Benchmark结尾;
- 参数须为 *testing.B;
- 基准测试函数不能有返回值;
- b.ResetTimer是重置计时器,这样能够防止for循环之前的初始化代码的烦扰;
- b.N是基准测试框架提供的,Go会依据零碎状况生成,不必用户设定,示意循环的次数,因为须要重复调用测试的代码,才能够评估性能;
运行:go test -bench=. -run=none 命令失去以下后果
运行benchmark基准测试也要用到 go test 命令,不过咱们前面须要加上-bench=参数,承受一个表达式作为参数,匹配基准测试的函数,"."一个点示意运行所有的基准测试。
因为默认状况下 go test 会运行单元测试,为了避免单元测试的输入影响咱们查看基准测试的后果,能够应用-run=匹配一个素来没有的单元测试办法,过滤掉单元测试的输入,咱们这里应用none,因为咱们基本上不会创立这个名字的单元测试办法。
接下来再解释下输入的后果:
- 函数名前面的-8,示意运行时对应的 GOMAXPROCS 的值;
- 接着的 1230048 示意运行 for 循环的次数,也就是调用被测试代码的次数,也就是在b.N的范畴内执行的次数;
- 最初的 112.9 ns/op示意每次须要破费 112.9 纳秒;
以上是测试工夫默认是1秒,也就是1秒的工夫,调用 1230048 次,每次调用破费 112.9 纳秒。如果想让测试运行的工夫更长,能够通过 -benchtime= 指定,比方-benchtime=3s,示意执行3秒。
然而咱们通过测试发现,测试1s和3s如同没啥显著区别,实际上最终性能并没有多大变动。一般来说不须要太长,罕用1s、3s、5s即可,也可忙依据业务场景来判断。
1.2 并行用法
func BenchmarkSprints(b *testing.B) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { // do something fmt.Sprint("代码轶事") } })}
- RunParallel并发的执行benchmark。RunParallel创立p个goroutine而后把b.N个迭代测试散布到这些goroutine上。
- goroutine的数目默认是GOMAXPROCS。如果要减少non-CPU-bound的benchmark的并个数,在执行RunParallel之前那就应用
b.SetParallelism(p int)
来设置,最终goroutine个数就等于p * runtime.GOMAXPROCS(0),。
numProcs := b.parallelism * runtime.GOMAXPROCS(0)
- 所以并行的用法比拟适宜IO密集型的测试对象。
1.3 性能比照
下面是简略写的几个示例,上面应用我后面的文章Go语言几种字符串的拼接形式比拟外面对于字符串拼接的例子进行示例:
// 文中全局有一个StrData变量,是一个200长度的字符串slice// 间接应用“+”号拼接func BenchmarkStringsAdd(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { var s string for _, v := range StrData { s += v } } b.StopTimer()}// fmt.Sprint拼接func BenchmarkStringsFmt(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { var _ string = fmt.Sprint(StrData) } b.StopTimer()}// strings.Join拼接func BenchmarkStringsJoin(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { _ = strings.Join(StrData, "") } b.StopTimer()}// StringsBuffer拼接func BenchmarkStringsBuffer(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { n := len("") * (len(StrData) - 1) for i := 0; i < len(StrData); i++ { n += len(StrData[i]) } var s bytes.Buffer s.Grow(n) for _, v := range StrData { s.WriteString(v) } _ = s.String() } b.StopTimer()}// 应用strings包里的builder类型拼接func BenchmarkStringsBuilder(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { n := len("") * (len(StrData) - 1) for i := 0; i < len(StrData); i++ { n += len(StrData[i]) } var b strings.Builder b.Grow(n) b.WriteString(StrData[0]) for _, s := range StrData[1:] { b.WriteString("") b.WriteString(s) } _ = b.String() } b.StopTimer()}
这次咱们增加-benchmem参数,go test -bench=. -benchmem -run=none,来查看每次操作分配内存的次数,运行后的后果如下:
从测试后果来看,strings包的Builder类型的效率是最高的,单次耗时最低,内存调配的次数起码,每次调配的内存最低。这样咱们就能从测试后果看进去是那局部代码最慢、内存调配占用太高,进而想方法对相应的代码进行优化解决。
在代码开发中,咱们很多时候是不须要太在乎性能的,但绝大部分时候是须要要求性能很高的,因而编写基准测试就变得十分重要。这能帮忙咱们开发出高性能、高效率的代码。
1.4 联合pprof和火焰图查看代码性能
咱们还是以1.3节的例子,以及Go语言几种字符串的拼接形式比拟里的例子来阐明一下benchmark联合pprof和火焰图查看代码性能的问题。
须要先采集数据,生成文件,而后用pprof关上文件并已http的形式查看,能够别离采集内存维度和CPU维度的数据,具体命令如下:
# 应用benchmark采集3秒的内存维度的数据,并生成文件go test -bench=. -benchmem -benchtime=3s -memprofile=mem_profile.out# 采集CPU维度的数据go test -bench=. -benchmem -benchtime=3s -cpuprofile=cpu_profile.out# 查看pprof文件,指定http形式查看go tool pprof -http="127.0.0.1:8080" mem_profile.outgo tool pprof -http="127.0.0.1:8080" cpu_profile.out# 查看pprof文件,间接在命令行查看go tool pprof mem_profile.out
咱们执行go tool pprof -http="127.0.0.1:8080" cpu_profile.out命令后,会主动应用咱们电脑的默认浏览器关上:http://127.0.0.1:8080/ui/地址,会显示默认的Graph选项卡,显示各办法间的调用关系:
图片不分明,次要表白意思,具体内容可依据本人的测试状况进行查看剖析。
而后咱们抉择左上角的菜单 VIEW->Flame Graph即可显示火焰图:
这里如果有的小伙伴没有提前装置好gvedit,可能会报错提醒须要装置graphviz。Mac或Linux用户可间接应用brew进行装置:
# Mac 装置brew install graphviz# Ubuntu apt-get 装置sudo apt-get install graphviz# yum 装置sudo yum install graphviz
Windows用户去官网下载http://www.graphviz.org/downl...
咱们也能够间接在命令行应用go tool pprof cpu_profile.out命令进行查看,
比方上图就是用命令行关上,而后输出top3命令来返回耗费资源最多的3个函数,而后你也能够输出help命令来查看反对的性能。
还有其它各种维度的指标和命令就不在此处多说了,前面也会出pprof的文章。
下面介绍了应用benchmark进行一个基准测试的一些根底用法, 当然了,如果你比拟卷,还是能够持续往下看,咱们来介绍一些进阶的用法。
2 深入研究benchmark
上面的内容,将会对一些不罕用然而很深刻的内容做一些阐明,有很多办法我也简直用不到,如有不对的中央还请留言斧正,感激!
2.1 Start/Stop/ResetTimer()
这三个办法都是针对计时统计器和内存统计器操作的。
因为有些状况咱们在做benchmark测试的时候,是不想将一些不关怀的工作耗时计算进benchmark后果中的。
比方我在1.3节中做出的示例,其实我在最开始的init()函数里设置了一个较大的slice:StrData。以便在全局应用同一个slice进行测试,然而我在设置这个较大的slice的时候也会内存的耗费和工作耗时,然而我并不关怀它的资源耗费,因而我也不心愿会对benchmark的测试后果产生影响,所以在每个被测单元里都执行了b.ResetTimer()。
而且须要留神的是,在并行的状况下,b.ResetTimer()须要在b.RunParallel()之前调用,如:
func BenchmarkSprints(b *testing.B) { b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { // do something fmt.Sprint("代码轶事") } })}
StopTimer()和StartTimer()的用法如下:
init();b.ResetTimer()for i := 0; i < b.N; i++ { flag := func1() if flag { // 须要计时 b.StartTimer() }else { // 不须要计时 b.StopTimer() }}
总结来说
- StartTimer:开始计时测试,该函数会被主动调用,也可用于在调用了StopTimer后复原计时;
- StopTimer:进行对测试计时,也可用于在执行简单的初始化时暂停计时;
- ResetTimer:将已用的基准工夫和内存调配计数器置零,并删除相干指标,但不影响计时器是否在运行;
2.2 benchmark的输入我的项目含意解释
咱们先尝试执行go test -bench=. -benchmem失去下图的输入后果:
接下来别离介绍每一项的含意;
- 第一项是事实的被测试的办法名,前面跟的“-8”示意P的个数,通过在命令前面追加参数“-cpu 4,8” 来指定;
- 第二项是指在b.N周期内迭代的总次数,即b.N的执行下限,通常程序执行效率越高,耗时越低,内存调配和耗费越小,迭代次数就越大;
- b.N每次迭代耗时,单位是ns,即每次迭代耗费多少ns,是一个被均匀后的均值;
- b.N每次迭代的内存调配,即在每次迭代中调配了多少字节的内存;
- b.N每次迭代所触发的内存调配次数,触发的内存调配次数越大,耗时多大,效率也就越低;
2.3 进阶参数
2.3.1 -benchtime t
咱们在测试某个函数性能的时候,并不是每次执行都会失去截然不同的成果,我间断执行10次,可能会有10次不一样的后果,这时候咱们可能会抉择增加一个指定的采样工夫,来得出一个平均值,在上文中咱们探讨benchmark联合pprof应用的时候就用到了这个参数,但也不是自觉的有限减少采样工夫就是好的,通常采纳3秒5秒即可。
该参数还可反对非凡模式Nx,用来指定被测函数的迭代次数,如:
从上图能够看出,指定了迭代100次,则每个函数都会只迭代100次。
2.3.2 -count n
为了咱们测试的准确性,能够增加-count来指定测试:
2.3.3 -cpu n
还能够指定cpu的核数,比方我上面的这个例子,应用递归实现一个计算斐波那契数列的办法,而后每次迭代都开启10个goroutine,并且要等这10个goroutine都执行完结后才会进行下一次迭代,代码如下:
func BenchmarkFibonacci(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { wg := sync.WaitGroup{} wg.Add(10) for i := 0; i < 10; i++ { go func(wg1 *sync.WaitGroup) { defer wg1.Done() arr := [20]int{} for i := 0; i < 20; i++ { arr[i] = fibonacci(i) } }(&wg) } }}func fibonacci(n int) (res int) { if n <= 1 { res = 1 } else { res = fibonacci(n-1) + fibonacci(n-2) } return}
而后别离指定-cpu=1,2,4,6,8,10来查看测试后果:
从运行后果来看,CPU外围数进步对性能有肯定影响,但也无奈始终实现正相干,而且超过肯定阈值后反而性能降落了,因为CPU外围的切换也须要老本。因而也不能自觉进步CPU外围数。
2.3.4 -benchmark
除了速度,内存调配也是一个很重要的指标,我在Go语言几种字符串的拼接形式比拟一文中做个比拟,在应用strings包的builder类型去做字符串拼接的时候,是否正当的预分配内存,测试的后果是不同的,如果咱们能正当的预分配内存,那么性能也会有较大的晋升。上面我再贴出一个例子来看理论的成果:
// 依据slice的长度,对strings.Builder进行预分配内存func BenchmarkStringsBuilder1(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { n := len("") * (len(StrData) - 1) for i := 0; i < len(StrData); i++ { n += len(StrData[i]) } var b strings.Builder b.Grow(n) b.WriteString(StrData[0]) for _, s := range StrData[1:] { b.WriteString("") b.WriteString(s) } _ = b.String() } b.StopTimer()}// 不进行预分配内存func BenchmarkStringsBuilder2(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { var b strings.Builder b.WriteString(StrData[0]) for _, s := range StrData[1:] { b.WriteString("") b.WriteString(s) } _ = b.String() } b.StopTimer()}
而后应用benchmark测试,查看后果:
BenchmarkStringsBuilder1是进行了正当的预分配内存,BenchmarkStringsBuilder2没有进行预分配内存,从测试的后果能够看出,BenchmarkStringsBuilder1的执行效率比BenchmarkStringsBuilder2的执行效率高了特地多。
3 benchmark原理
要探讨benchmark基准测试的原理,就要探讨testing.B的数据结构,还要剖析b.N的值,尽管官网材料中说b.N的值会主动调整,以保障牢靠的计时,但仍需剖析其实现的机制。
那么咱们抛出以下问题:
- b.N是如何主动调整的?
- 内存统计是如何实现的?
- SetBytes()其应用场景是什么?
原理局部的探讨参考了【Go专家编程】的一些文章,能够点击关键词去看在线版本。
3.1 testing.B的数据结构
源码包src/testing/benchmark.go:B
定义了性能测试的数据结构,咱们提取其比拟重要的一些成员进行剖析:
type B struct { common // 与testing.T共享的testing.common,负责记录日志、状态等,详情可见src/testing/testing.go文件,在大略385行 importPath string // import path of the package containing the benchmark context *benchContext N int // 指标代码执行次数,会主动调整 previousN int // number of iterations in the previous run previousDuration time.Duration // total duration of the previous run benchFunc func(b *B) // 性能测试函数 benchTime time.Duration // 性能测试函数起码执行的工夫,默认为1s,能够通过参数'-benchtime 10s'指定 bytes int64 // 每次迭代解决的字节数 missingBytes bool // one of the subbenchmarks does not have bytes set. timerOn bool // 是否已开始计时 showAllocResult bool result BenchmarkResult // 测试后果 parallelism int // RunParallel creates parallelism*GOMAXPROCS goroutines // The initial states of memStats.Mallocs and memStats.TotalAlloc. startAllocs uint64 // 计时开始时堆中调配的对象总数 startBytes uint64 // 计时开始时时堆中调配的字节总数 // The net total of this test after being run. netAllocs uint64 // 计时完结时,堆中减少的对象总数 netBytes uint64 // 计时完结时,堆中减少的字节总数 extra map[string]float64 // 额定收集的指标}
其次要成员如下:
- common: 与testing.T共享的testing.common,治理着日志、状态等;
- N:每个测试中用户代码执行次数
- benchFunc:测试函数
- benchTime:性能测试起码执行工夫,默认为1s,能够通过能数-benchtime 2s指定
- bytes:每次迭代解决的字节数
- timerOn:计时启动标记,默认为false,启动计时为true
- startAllocs:测试启动时记录堆中调配的对象数
- startBytes:测试启动时记录堆中调配的字节数
- netAllocs:测试完结后记录堆中新减少的对象数,公式:完结时堆中调配的对象数-
- netBytes:测试对预先记录堆中新减少的字节数
流程示意图如下
5 参考文献
https://books.studygolang.com...