go资深之路笔记(八) 基准测试
一、用法
aa_test.go
func BenchmarkAdd(b *testing.B) {a := "shdhAAsaBBjhsd"for i := 0; i < b.N; i++ {res := strings.Replace(a, "AA", "BB", -1)_ = res}
}
然后执行测试:
go test --bench=BenchmarkAdd --benchmem aa_test.go
输出:
细节分析:
- 脚本名必须是 xxx_test.go
- 函数名必须是 Benchmark开头
- 函数参数必须是 *testing.B
- _ = res 是为了防止go编译器进行优化(当然去掉编译也不会通过)
- –bench 是指定函数,他的值是正则表达式。 --bench=. 是匹配所有基准函数
- –benchmem 是分析内存分配
- 参数前的两个横杠,也可以用一个。 --bench等于-bench【不得不说go的设计真的很人性】
- 图片红框第一个 执行次数,剩下三个是每次执行时消耗时间,分配内存字节,分配内存次数
二、参数大全
# 启用内存分配统计
go test -bench=. -benchmem### 性能分析 ##################
# 指定测试运行时间
go test -bench=. -benchtime=5s # 运行5秒
go test -bench=. -benchtime=100x # 运行100次迭代 b.N = 100# 一键执行多次
go test -bench=. -count=10# 并行运行测试(cpu多不一定变快,除非有多goroutine任务)
## 这里要说一下有意思的点,开一个goroutine的 2-4KB 栈内存以及一定时间,如果对应耗时比较少的任务,
##其实根本没必要开 goroutine,因为你开goroutine有可能耗时更多,内存更多是绝对的,所以goroutine不是万金油。
go test -bench=. -cpu=4### cpu 内存分析 借助 pprof ##################
# 生成 CPU profile
go test -bench=. -cpuprofile=cpu.pprof# 生成内存 profile
go test -bench=. -benchmem -memprofile=mem.pprof# 生成阻塞 profile
go test -bench=. -blockprofile=block.pprof# 生成互斥锁 profile
go test -bench=. -mutexprofile=mutex.pprof#### 并发和调度相关 #################
# 指定不同的 GOMAXPROCS 值
go test -bench=. -cpu=1,2,4,8# 设置并行度(与 -cpu 不同)
go test -bench=. -parallel=8# 跟踪调度信息(调试用)
go test -bench=. -trace=trace.out
2.1 b.RunParallel
看了上面的参数,你大概知道想要实现并发效果,并不是加go就行的,这里可以用
b.RunParallel(这个函数的内部实现大概是启用goroutine的数量等同于GOMAXPROCS(go可用核心数),或者GOMAXPROCS的倍数;从而实现cpu核心的充分使用。)
func cal(a string) {res := strings.Replace(a, "AA", "BB", -1)_ = res
}// var res = ""
var chan1 = make(chan struct{}, 8)func BenchmarkAdd(b *testing.B) {a := "shdhAAsaBBjhsd"b.RunParallel(func(pb *testing.PB) {for pb.Next() {cal(a)}})
}
执行:
go test -bench=BenchmarkAdd -benchmem -cpu=1,2,4,8,16,24 aa_test.go
效果:
2.2 有缓存的channel
有个很有意思的点:现实环境下,不可能让goroutine一直上涨,特别是cpu密集型任务,不限制的话会内存和cpu会爆炸性增长!!(for go func() 就会如此, b.RunParallel 不会是因为限制了goroutin数量)但是一般普通的限制方式,比如 用有缓存的channel来限制,你会发现cpu不是越大越好:
func cal(a string) {defer func() {<-chan1}()res := strings.Replace(a, "AA", "BB", -1)_ = res
}var chan1 = make(chan struct{}, 8)func BenchmarkAdd(b *testing.B) {a := "shdhAAsaBBjhsd"//for i := 0; i < b.N; i++ { // go cal(a)//}b.SetParallelism(10)b.RunParallel(func(pb *testing.PB) {for pb.Next() {chan1 <- struct{}{} // 用channel来控制同时并发goroutine数量cal(a)}})
}
执行你会发现cpu越大你会:
原因是: 有缓存的channel 内部构造是 有锁的,也就是说 存在多个cpu争抢同一个内存的情况,这个时候可能会涉及缓存一致性,以及获取锁过程中阻塞或者自旋而产生的消耗。
这就产生了两个问题:
- channel限流的操作并不一定适合,可以优化;【这个之后的文章会再写,篇幅不够】
- 并不是cpu数量越大约好的。(这个其实再上一个例子的BenchmarkAdd-24 也能看出来)
2.3 总结:
这是探索命令过程中的一些笔记,可能有点乱,稍微总结下
- b.RunParallel 函数可以用来测试代码在那个核心数下运行最佳(也可以测是否panic)
- 用有缓存channel 来限流会导致争抢,cpu核心数越高越明显(可优化)
- 基准测试的时候其实是没有计算goroutine创建的消耗和内存的,实际上一个goroutine创建占用内存 是2kB~4kB, 所以除了io密集型或者cpu密集型任务,有时候可能用同步的方式执行会更快
2.4 和 pprof联合工作
就是上面的命令
# 生成 CPU profile
go test -bench=. -cpuprofile=cpu.pprof# 生成内存 profile
go test -bench=. -benchmem -memprofile=mem.pprof# 生成阻塞 profile
go test -bench=. -blockprofile=block.pprof# 生成互斥锁 profile
go test -bench=. -mutexprofile=mutex.pprof
需要先安装 graphviz(火焰图生成工具)
sudo apt-get install graphviz
直接用 pprof工具打开再网页展示
go tool pprof -http=localhost:8080 cpu.out
2.5 和 trace联合工作
生成 trace测试文件
go test -bench=. -benchtime=10000x -trace=trace.out
用trace工具打开文件
go tool trace trace.out
ps:-benchtime=10000x 代表执行10000次,因为不指定的话一个测试动则几十上百万次,trace根本打不开
三、坑点:
1.循环开始前有耗时也会被计入,我们可以用 b.ResetTimer() 重置计时器,排除上方初始化时间
或者用 b.StopTimer() 和 b.StartTimer() 开始计时
-
如果在 b.Loop() 或 b.N 循环中使用了 break 等语句提前退出,会导致实际迭代次数远少于预期,测试结果严重失准,且框架可能不会告警。
-
善用 benchtime 和count: 测量非常快(纳秒级)的操作时,单次测试结果极易受机器负载、电源管理等环境因素干扰,导致结果不稳定。
使用 -benchtime 延长单次测试时间,例如 -benchtime=5s。
使用 -count 多次运行测试,并结合 benchstat 工具进行统计分析
benchstat 用法:
go test -bench=BenchmarkAdd -benchmem -count=5 aa_test.go | tee stats.txt
benchstat stats.txt