在 Go 中 Benchmark 是常用的性能对比手段,可以辅助我们优化代码。
Benchmark 结构讲解
目录结构
本次实验的目录结构:
- ~/hello via 🐹
- ❯ tree
- .
- ├── fib
- │ ├── fib.go
- │ └── fib_test.go
- └── encode
- └── encode_test.go
创建文件,在一文件夹下创建文件fib.go
,创建fib_test.go
作为测试文件。
- ~/hello via 🐹
- ❯ ll fib
- total 16
- -rw-r--r-- 1 ch staff 102B Oct 19 18:14 fib.go
- -rw-r--r-- 1 ch staff 258B Oct 19 18:15 fib_test.go
代码内容
fib.go
的文件内容:
- ❯ cat fib/fib.go
- package fib
- func fib(n int) int {
- if n == 0 || n == 1 {
- return n
- }
- return fib(n-2) + fib(n-1)
- }
fib_test.go
的文件内容:
package fib
import "testing"
func BenchmarkFib10(b *testing.B) {
b.ResetTimer()
for n := 0; n < b.N; n++ {
fib(10)
}
}
func BenchmarkFib20(b *testing.B) {
b.ResetTimer()
for n := 0; n < b.N; n++ {
fib(20)
}
}
Benchmark 执行
常用执行方式
go test -bench=Fib ./fib
// 执行 fib 文件下,函数名包涵 Fib 的方法
go test -bench=Fib -benchtime=2x ./fib
// 执行 2 次
go test -bench=Fib -benchtime=1s ./fib
// 执行 1 秒
go test -bench=Fib -count=2 ./fib
// 函数测试 2 次
go test -bench=Fib -cpu=1,2,4,8 ./fib
// cpu 核数限制
go test -bench=Fib -benchmen ./fib
// 展示内存分配
参数解释
参数 | 🌰 | 解释 |
---|---|---|
-bench=* | -banch=Fib | 支持正则表达式,只有匹配到的测试用例才会执行,使用. 则运行所有测试用例 |
-count=n | -count=2 | 函数测试轮数 |
-benchtime=Ns | -benchtime=1s | 函数运行时间 |
-benchtime=Nx | -benchtime=2x | 函数运行次数 |
-cpu=* | -cpu=1,2,4,8 | 指定cpu核数 |
-benchmem | - | 展示内存使用情况 |
结果分析
执行上面的 benchmark:
- ❯ go test -bench=Fib -benchmem ./fib
- goos: darwin
- goarch: amd64
- pkg: ~/hello/fib
- cpu: Intel(R) Core(TM) i7-4770HQ CPU @ 2.20GHz
- BenchmarkFib10-8 2968881 402.1 ns/op 0 B/op 0 allocs/op
- BenchmarkFib20-8 23586 57676 ns/op 0 B/op 0 allocs/op
- PASS
针对上面的结构进行解释,以BenchmarkFib10-8
这行为例子。
参数 | 含义 |
---|---|
goos | 系统os |
gorach | 系统架构 |
pkg | package 名称 |
cpu | cpu 处理器型号 |
BenchmarkFib10-8 | 函数名-cpu核数 |
2968881 | 迭代次数 |
402.1 ns/op | 每次操作耗费的纳秒 |
0 B/op | 每次操作分配的bytes |
0 allocs/op | 每次操作的分配次数 |
实践
现有一业务需要用到json.Encode
方法,那么可以benchmark下。
代码如下
import(
"encoding/gob"
"encoding/json"
"github.com/francoispqt/gojay"
jsoniter "github.com/json-iterator/go"
)
func BenchmarkGobEncode(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
var buffer bytes.Buffer
encoder := gob.NewEncoder(&buffer)
encoder.Encode(doc)
}
}
func BenchmarkJsonEncode(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
var buffer bytes.Buffer
encoder := json.NewEncoder(&buffer)
encoder.Encode(doc)
}
}
func BenchmarkJsoniterEncode(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
var buffer bytes.Buffer
encoder := json.NewEncoder(&buffer)
encoder.Encode(doc)
}
}
func BenchmarkGojayEncode(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
var buffer bytes.Buffer
encoder := gojay.NewEncoder(&buffer)
encoder.Encode(doc)
}
}
有时基准测试的设置成本是每次运行一次。b.ResetTimer()可以用来忽略setup中累积的时间。
benchmark结果:
- ❯ go test -bench=Encode -benchmem ./encode
- goos: darwin
- goarch: amd64
- pkg: ~/hello/encode
- cpu: Intel(R) Core(TM) i7-4770HQ CPU @ 2.20GHz
- BenchmarkGobEncode-8 265953 4194 ns/op 1664 B/op 31 allocs/op
- BenchmarkJsonEncode-8 1503925 745.3 ns/op 240 B/op 3 allocs/op
- BenchmarkJsoniterEncode-8 1593012 741.6 ns/op 240 B/op 3 allocs/op
- BenchmarkGojayEncode-8 2765398 428.9 ns/op 288 B/op 5 allocs/op
- PASS
- ok ~/hello/encode 7.019s
可以通过结果对比BenchmarkGojayEncode
的方式是最优解。
Benchmark 其他玩法
benchstat
benchstat
对benchmakr
进行计算统计摘要和A/B比较。
测试手段
我们以BenchmarkGobEncode
与BenchmarkGojayEncode
为例子,做对比。
这里为了对比性能提升,我们需要修改代码,BenchmarkGobEncode
函数名改为BenchmarkEncode
, 执行 10 次benchmark
- ❯ go test -bench=BenchmarkEncode -count=10 ./encode | tee old.txt
- goos: darwin
- goarch: amd64
- pkg: ~/hello/encode
- cpu: Intel(R) Core(TM) i7-4770HQ CPU @ 2.20GHz
- BenchmarkEncode-8 264470 4294 ns/op
- BenchmarkEncode-8 285097 4109 ns/op
- BenchmarkEncode-8 274879 4064 ns/op
- BenchmarkEncode-8 257868 4069 ns/op
- BenchmarkEncode-8 293433 4107 ns/op
- BenchmarkEncode-8 277754 4006 ns/op
- BenchmarkEncode-8 294042 4064 ns/op
- BenchmarkEncode-8 277792 5215 ns/op
- BenchmarkEncode-8 280048 4075 ns/op
- BenchmarkEncode-8 284413 4132 ns/op
- PASS
- ok ~/hello/encode 12.790s
查看分析
- ❯ benchstat gob.txt
- goos: darwin
- goarch: amd64
- pkg: ~/hello
- cpu: Intel(R) Core(TM) i7-4770HQ CPU @ 2.20GHz
- │ gob.txt │
- │ sec/op │
- GobEncode-8 4.091µ ± 5%
benchstat告诉我们,BenchmarkGobEncode平均值为4.091微秒,样品中的+/- 5%变化。
同理,这里为了对比性能提升,我们需要修改代码,BenchmarkGojayEncode
函数名改为BenchmarkEncode
, 执行 10 次benchmark
- ❯ go test -bench=BenchmarkEncode -count=10 ./encode | tee new.txt
- goos: darwin
- goarch: amd64
- pkg: ~/hello/encode
- cpu: Intel(R) Core(TM) i7-4770HQ CPU @ 2.20GHz
- BenchmarkEncode-8 2773693 414.5 ns/op
- BenchmarkEncode-8 2768872 415.9 ns/op
- BenchmarkEncode-8 2870995 430.7 ns/op
- BenchmarkEncode-8 2735547 422.8 ns/op
- BenchmarkEncode-8 2896785 420.2 ns/op
- BenchmarkEncode-8 2807527 427.5 ns/op
- BenchmarkEncode-8 2844328 426.5 ns/op
- BenchmarkEncode-8 2859422 414.5 ns/op
- BenchmarkEncode-8 2926161 419.6 ns/op
- BenchmarkEncode-8 2764474 416.2 ns/op
- PASS
- ok ~/hello/encode 16.668s
查看分析
- ❯ benchstat gob.txt
- goos: darwin
- goarch: amd64
- pkg: ~/hello
- cpu: Intel(R) Core(TM) i7-4770HQ CPU @ 2.20GHz
- │ gojay.txt │
- │ sec/op │
- GojayEncode-8 419.9n ± 2%
benchstat告诉我们,BenchmarkGojayEncode平均值为419.9纳秒,样品中的+/- 2%变化。
结果分析
使用benchstat
对old.txt
与new.txt
进行对比。
- ❯ benchstat old.txt new.txt
- goos: darwin
- goarch: amd64
- pkg: g101/hello/indexs
- cpu: Intel(R) Core(TM) i7-4770HQ CPU @ 2.20GHz
- │ old.txt │ new.txt │
- │ sec/op │ sec/op vs base │
- Encode-8 4093.5n ± 2% 442.1n ± 12% -89.20% (p=0.000 n=10)
benchstat将为每种配置打印单独的表,比较表明提升了89.2%
,但是新修改的波动更大12%
,也可能是样本数据太少导致。
p值小于0.05可能具有统计学意义。p值大于0.05意味着该基准可能不具有统计显著性
Benchmark 错误
下面的代码是错误的,请谨慎使用!
// 错误使用
func BenchmarkFibWrong(b *testing.B) {
Fib(b.N)
}
// 错误使用
func BenchmarkFibWrong2(b *testing.B) {
for n := 0; n < b.N; n++ {
Fib(n)
}
}
Profiling benchmarks
测试包内置了对生成CPU、memory和block profile文件的支持。
-
cpuprofile=$FILE, 写一个 CPU profile 的文件;
-
memprofile=$FILE, 写一个 memory profile 的文件, -memprofilerate=N 调整 profile 速率为 1/N;
-
blockprofile=$FILE, 写一个 block profile 的文件;
测试方式
代码如下:
func BenchmarkEncode(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
var buffer bytes.Buffer
// old
// encoder := gob.NewEncoder(&buffer)
// encoder.Encode(doc)
// new
// encoder := gojay.NewEncoder(&buffer)
// encoder.Encode(doc)
}
}
gob 方式抓取 cpu profile。
- ❯ go test -bench=BenchmarkEncode -cpuprofile=old.p ./encode
- goos: darwin
- goarch: amd64
- pkg: ~/hello/encode
- cpu: Intel(R) Core(TM) i7-4770HQ CPU @ 2.20GHz
- BenchmarkEncode-8 232494 4416 ns/op
- PASS
- ok ~/hello/encode 1.753s
gojay 方式抓取 cpu profile。
- ❯ go test -bench=BenchmarkEncode -cpuprofile=new.p ./encode
- goos: darwin
- goarch: amd64
- pkg: ~/hello/encode
- cpu: Intel(R) Core(TM) i7-4770HQ CPU @ 2.20GHz
- BenchmarkEncodeOld-8 2654364 465.5 ns/op
- PASS
- ok ~/hello/encode 2.153s
对比结果
- ❯ go tool pprof -http=:8888 -base old.p new.p
- Serving web UI on http://localhost:8888
得到对比结果: