或许你经常会思考这样的问题,我用不同的方法实现了同样的效果,哪个会更快?哪个内存消耗更小?这时候你一个简单的基准测试就能解决你的疑惑。

Go向来是以工具丰富而著称的,在学习Go的过程中,你会发现无论是写一个单元测试,还是做一些竞争检测都能很快的上手,而且用的很痛快。当然,接下来要说的基准测试也一样。

基准测试工具就在Go的测试包中,下面就用一个例子来介绍。

举个栗子

由于一些场景需要,我需要将[]byte输出16进制字符。

有时候我会这么写:

fmt.Sprintf("%x", b)

但有时候我会这么写:

hex.EncodeToString(b)

但到底哪种写法更好呢?今天我就来比较一下。

直接写了个main.go

func EncodeA(b []byte) string {
	return fmt.Sprintf("%x", b)
}

func EncodeB(b []byte) string {
	return hex.EncodeToString(b)
}

再写个测试main_test.go

var buf = []byte("skdjadialsdgasadasdhsakdjsahlskdjagloqweiqwo")

func BenchmarkEncodeA(b *testing.B) {
	for i := 0; i < b.N; i++ {
		EncodeA(buf)
	}
}

func BenchmarkEncodeB(b *testing.B) {
	for i := 0; i < b.N; i++ {
		EncodeB(buf)
	}
}

就这么简单,我们的基本测试就写完了。从我的写法中你也许就知道:

  • 和单元测试一样,都写在_test.go文件中;
  • 需要以Benchmark为函数名开头;
  • 和单元测试类似,必须接受一个*testing.B参数;
  • 被测试代码放在一个循环中。

我们直接跑一下。当然我们也是用go test来执行测试,简单的测试只要带上-bench=.就可以了。

$ go test -bench=.
goos: darwin
goarch: amd64
pkg: github.com/razeencheng/demo-go/benchmark
BenchmarkEncodeA-8       5000000               265 ns/op
BenchmarkEncodeB-8      10000000               161 ns/op
PASS
ok      github.com/razeencheng/demo-go/benchmark        3.397s

前两行是平台信息,第三行包名。第四、五行就是测试的结果了。

  • BenchmarkEncodeA-8 ,BenchmarkEncodeB-8 基准测试函数名-GOMAXPROCS
  • 5000000,10000000 被测试的函数执行次数,也就是EncodeA()被执行了5000000次,EncodeB()被执行了10000000次,也就是b.N的值了。
  • 265 ns/op,161 ns/op表示每次调用被测试函数花费的时间。

从花费的时间上来看,我们知道EncodeB()要快一点。

更多

你以为就这么简单的结束了么?NONONO。

  • -bench 可接收一个有效的正则表达式来执行符合条件的测试函数。当你的函数很多时,可以用它来过滤.
$ go test -bench=BenchmarkEncodeA
goos: darwin
goarch: amd64
pkg: github.com/razeencheng/demo-go/benchmark
BenchmarkEncodeA-8       5000000               256 ns/op
PASS
ok      github.com/razeencheng/demo-go/benchmark        1.575s
  • -benchmem可以查看内存分配
$ go test -bench=. -benchmem
goos: darwin
goarch: amd64
pkg: github.com/razeencheng/demo-go/benchmark
BenchmarkEncodeA-8       5000000               261 ns/op             128 B/op          2 allocs/op
BenchmarkEncodeB-8      10000000               162 ns/op             192 B/op          2 allocs/op
PASS
ok      github.com/razeencheng/demo-go/benchmark        3.408s

其中B/op 表示每次执行会分配多少内存,allocs/op表示每次执行会发生多少次内存分配。

  • -benchtime指定每个测试执行的时间。默认1s,当你的函数比较耗时你可以设置更长一点。因为b.N是与这个时间有关的。

    当你的运行时间没达到-benchtime制定的时间前,b.N将以1,2,5,10,20,50…增加,然后重新运行测试代码。

    $ go test -bench=. -benchmem -benchtime=5s
    goos: darwin
    goarch: amd64
    pkg: github.com/razeencheng/demo-go/benchmark
    BenchmarkEncodeA-8      30000000               254 ns/op             128 B/op          2 allocs/op
    BenchmarkEncodeB-8      50000000               160 ns/op             192 B/op          2 allocs/op
    PASS
    ok      github.com/razeencheng/demo-go/benchmark        16.113s  
    
  • -count指定每个测试执行的次数。

$ go test -bench=. -benchmem -count=3
goos: darwin
goarch: amd64
pkg: github.com/razeencheng/demo-go/benchmark
BenchmarkEncodeA-8       5000000               256 ns/op             128 B/op          2 allocs/op
BenchmarkEncodeA-8       5000000               255 ns/op             128 B/op          2 allocs/op
BenchmarkEncodeA-8       5000000               253 ns/op             128 B/op          2 allocs/op
BenchmarkEncodeB-8      10000000               163 ns/op             192 B/op          2 allocs/op
BenchmarkEncodeB-8      10000000               160 ns/op             192 B/op          2 allocs/op
BenchmarkEncodeB-8      10000000               160 ns/op             192 B/op          2 allocs/op
PASS
ok      github.com/razeencheng/demo-go/benchmark        9.984s

我常用的也就这些了。

但对于testing.B来说,它拥有了testing.T的全部接口,所以Fail,Skip,Error这些都可以用,而且还增加了

  • SetBytes( i uint64) 统计内存消耗。
  • SetParallelism(p int) 制定并行数目。
  • StartTimer / StopTimer / ResertTimer 操作计时器。

你可以按需使用。

注意

b.N为一个自增字段,谨慎用它做函数参数。