共计 8180 个字符,预计需要花费 21 分钟才能阅读完成。
在日常开发中,基准测试是必不可少的,基准测试次要是通过测试 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.out
go 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…