我的名字叫浩仔/Go Benchmark 使用

Created Thu, 19 Oct 2023 19:48:12 +0800 Modified Tue, 24 Oct 2023 16:43:02 +0800

在 Go 中 Benchmark 是常用的性能对比手段,可以辅助我们优化代码。

Benchmark 结构讲解

目录结构

本次实验的目录结构:

x
+
~/hello(~zsh):
  • ~/hello via 🐹
  • ❯ tree
  • .
  • ├── fib
  • │   ├── fib.go
  • │   └── fib_test.go
  • └── encode
  •    └── encode_test.go

创建文件,在一文件夹下创建文件fib.go,创建fib_test.go作为测试文件。

x
+
~/hello(~zsh):
  • ~/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的文件内容:

x
+
~/hello(~zsh):
  • ❯ 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:

x
+
~/hello/fib(~zsh):
  • ❯ 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结果:

x
+
~/hello/fib(~zsh):
  • ❯ 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

benchstatbenchmakr进行计算统计摘要和A/B比较。

下载 benchstat,详细教程查看这里。

测试手段

我们以BenchmarkGobEncodeBenchmarkGojayEncode为例子,做对比。

这里为了对比性能提升,我们需要修改代码,BenchmarkGobEncode函数名改为BenchmarkEncode, 执行 10 次benchmark

x
+
~/hello/encode(~zsh):
  • ❯ 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

查看分析

x
+
~/hello(~zsh):
  • ❯ 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

x
+
~/hello/encode(~zsh):
  • ❯ 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

查看分析

x
+
~/hello(~zsh):
  • ❯ 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%变化。

结果分析

使用benchstatold.txtnew.txt进行对比。

x
+
~/hello(~zsh):
  • ❯ 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。

x
+
~/hello(~zsh):
  • ❯ 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。

x
+
~/hello(~zsh):
  • ❯ 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

对比结果

x
+
~/hello(~zsh):
  • ❯ go tool pprof -http=:8888 -base old.p new.p
  • Serving web UI on http://localhost:8888

得到对比结果:

profile

参考资料

JS
Arrow Up