go基准测试bench

一、使用 testing 包构建基准测试


我们在main.go里面实现了一个Fib3函数
func Fib3(n int) int {
   switch n {
   case 0:
      return 0
   case 1:
      return 1
   case 2:
      return 1
   default:
      return Fib3(n-1) + Fib3(n-2)
   }
}

然后再同级目录新建一个main_test.go文件,然后写测试

package main

import "testing"

func BenchmarkFib320(b *testing.B) {
   for n := 0; n < b.N; n++ {
      Fib3(20)
   }
}

执行一个包下的基准测试

go test -bench=要测试的哪个函数(.就是默认目录中所有的)   测试文件所在目录

$go test -bench=. ./
goos: windows
goarch: amd64
pkg: go-test
BenchmarkFib320-12         52998             21527 ns/op
PASS
ok      go-test 1.628s

基准测试工作原理

b.N 从 1 开始,如果基准函数在 1 秒内就执行完了,那么 b.N 的值会递增以便基准函数再重新执行,即基准函数默认要运行 1 秒,如果该函数的执行时间在 1 秒内就运行完了,那么就递增 b.N 的值,重新再执行一次

在上面的 BenchmarkFib320-12的例子中,我们发现迭代大约 52998 次耗时超过了 1 秒。依据此数据,基准框架计算得知,平均每次运行耗时 21527 纳秒。

-12的意思是和运行该测试用例时的GOMAXPROCS值有关系。和GOMAXPROCS一样,此数字默认为启动时 Go 进程可见的 CPU 数。然后我们可以定制cpu的数量

$ go test -bench=.  -cpu=1,2,4  ./
goos: windows
goarch: amd64
pkg: go-test
BenchmarkFib320            56871             21330 ns/op
BenchmarkFib320-2          55562             21636 ns/op
BenchmarkFib320-4          55633             21213 ns/op
PASS
ok      go-test 4.531s
 

该示例展示了分别用 CPU 为 1 核、2 核、4 核时运行基准测试的结果。在该案例中,该参数对结果> > 几乎没有影响,因为该基准测试的代码是完全顺序执行的。

go1.13中基准测试框架的一些改变

在 Go1.13 版本之前,基准测试的迭代次数四舍五入为 1、2、3、5 的序列增长,这种四舍五入的初衷是使更便于肉眼阅读(make it easier to eyeball times)。然而,正确的分析都需要工具才能进行,因此,随着工具的改进,人们容易理解的数字变得不那么有价值。

四舍五入可能会隐藏一个数量级的变化。

幸运的是,在 Go 1.13 版本中,四舍五入的方式已经被移除了,这提高了在低的单位操作耗时(ns/op) 的准确性,并随着基准测试框架更快的达到正确的迭代次数而减少了整体基准测试的运行时间。

改进基准测试的准确性

可以使用-benchtime标识增加基准测试执行的时间的方式来增加迭代的次数。例如:

$ go test -bench=. -benchtime=10s ./
goos: windows
goarch: amd64
pkg: go-test
BenchmarkFib320-12        530166             21753 ns/op
PASS
ok      go-test 12.032s
运行相同的基准测试,直到其达到 b.N 的值需要花费超过 10 秒的时间才能返回。由于我们的运行时间增加了 10 倍,因此迭代的总次数也增加了 10 倍。结果(每次操作耗时 21753 ns/op) 没有太大的变化,这就是我们所期望的。

为什么总的耗时是 13 秒,而不是 10 秒呢? 如果你又一个基准测试运行了数百万次或数十亿次迭代,导致每次操作的时间都在微秒或纳秒范围内,则你可能会发现基准值不稳定,因为你的机器硬件的散热性能、内存局部性、后台进程、gc 等因素。

对于每次操作在 10 纳秒以下的,指令重新排序,并且代码对齐的相对论效应将影响基准时间

通过-count 标志,可以指定基准测试多跑几次:

$ go test -bench=Fib320 -count=10 ./
goos: windows
goarch: amd64
pkg: go-test
BenchmarkFib320-12         54190             21745 ns/op
BenchmarkFib320-12         54694             21786 ns/op
BenchmarkFib320-12         54883             21598 ns/op
BenchmarkFib320-12         55666             21826 ns/op
BenchmarkFib320-12         53163             22352 ns/op
BenchmarkFib320-12         49388             21555 ns/op
BenchmarkFib320-12         57067             22012 ns/op
BenchmarkFib320-12         55051             22051 ns/op
BenchmarkFib320-12         56857             21771 ns/op
BenchmarkFib320-12         55320             21873 ns/op
PASS
ok      go-test 14.452s
 

避免基准测试的启动耗时

有时候你的基准测试每次执行的时候会有一次启动配置耗时。b.ResetTimer() 函数可以用于忽略启动的累积耗时

func BenchmarkFib320(b *testing.B) {
   //加载配置文件
   //time.Sleep(time.Second * 5)
   for i := 0; i < 1000; i++ {
      fmt.Printf("1")
   }
   b.ResetTimer()
   for n := 0; n < b.N; n++ {
      Fib3(20)
   }
}

 

如果在每次循环迭代中,你有一些费时的配置逻辑,要使用 b.StopTimer() 和 b.StartTimer() 函数来暂定基准测试计时器。

func BenchmarkComplicated(b *testing.B) {
   for n := 0; n < b.N;n++ {
      b.StopTimer()
      complicatedSetup()
      b.StartTimer()
      //function under test
   }
}
  • 上例中,先使用 b.StopTimer() 暂停计时器
  • 然后执行完复杂的配置逻辑后,再使用 b.StartTimer() 启动计时器

通过以上两个函数,则可以忽略掉启动配置所耗费的时间。

 

基准测试的内存分配

内存分配的次数和分配的大小和基准测试的执行时间强相关。你可以通过在代码中增加 b.ReportAllocs() 函数来告诉 testing 框架记录内存分配的数据。

func BenchmarkRead(b *testing.B) {
   b.ReportAllocs()
   for n := 0; n < b.N; n++ {
      //function under test
   }
}

下面是使用 bufio 包中的基准测试的一个示例:

$ go test -run=^$ -bench=. bufio
goos: windows
goarch: amd64
pkg: bufio
BenchmarkReaderCopyOptimal-12           16476342                71.7 ns/op
BenchmarkReaderCopyUnoptimal-12         10020408               120 ns/op
BenchmarkReaderCopyNoWriteTo-12           325192              3425 ns/op
BenchmarkReaderWriteToOptimal-12         4662620               256 ns/op
BenchmarkWriterCopyOptimal-12           15863991                76.7 ns/op
BenchmarkWriterCopyUnoptimal-12         11744727               102 ns/op
BenchmarkWriterCopyNoReadFrom-12          343767              3484 ns/op
BenchmarkReaderEmpty-12                  1813867               663 ns/op            4224 B/op          3 allocs/op
BenchmarkWriterEmpty-12                  1872644               608 ns/op            4096 B/op          1 allocs/op
BenchmarkWriterFlush-12                 100000000               11.5 ns/op             0 B/op          0 allocs/op
PASS
ok      bufio   14.109s
 

你也可以使用 go test -benchmem 标识来强制 testing 框架打印出所有基准测试的内存分配次数

$ go test -run=^$ -bench=. -benchmem bufio
goos: windows
goarch: amd64
pkg: bufio
BenchmarkReaderCopyOptimal-12           16482700                71.4 ns/op            16 B/op          1 allocs/op
BenchmarkReaderCopyUnoptimal-12          9693397               125 ns/op              32 B/op          2 allocs/op
BenchmarkReaderCopyNoWriteTo-12           364612              3419 ns/op           32800 B/op          3 allocs/op
BenchmarkReaderWriteToOptimal-12         4584531               262 ns/op              16 B/op          1 allocs/op
BenchmarkWriterCopyOptimal-12           15191007                78.1 ns/op            16 B/op          1 allocs/op
BenchmarkWriterCopyUnoptimal-12         11519530               104 ns/op              32 B/op          2 allocs/op
BenchmarkWriterCopyNoReadFrom-12          364611              3371 ns/op           32800 B/op          3 allocs/op
BenchmarkReaderEmpty-12                  1674300               695 ns/op            4224 B/op          3 allocs/op
BenchmarkWriterEmpty-12                  1871054               614 ns/op            4096 B/op          1 allocs/op
BenchmarkWriterFlush-12                 100000000               11.6 ns/op             0 B/op          0 allocs/op
PASS
ok      bufio   14.313s

 

注意编译器的优化

package main

import "testing"

const m1 = 0x5555555555555555
const m2 = 0x3333333333333333
const m4 = 0x0f0f0f0f0f0f0f0f
const h01 = 0x0101010101010101

func popcnt(x uint64) uint64 {
   x -= (x >> 1) & m1
   x = (x & m2) + ((x >> 2) & m2)
   x = (x + (x >> 4)) & m4
   return (x * h01) >> 56
}

func BenchmarkPopcnt(b *testing.B) {
   for i := 0; i < b.N; i++ {
      popcnt(uint64(i))
   }
}

你认为这个基准测试的性能到底有多快呢?让我们看下面结果

$ go test -bench=. ./
goos: windows
goarch: amd64
pkg: go-test
BenchmarkPopcnt-12      1000000000               0.242 ns/op
PASS
ok      go-test 0.543s
0.242 ns 基本上就是一个 cpu 时钟的时间。即使假设每个时钟周期中 CPU 有一些指令要运行,但这个数字看起来也有点不太合理。那到底发生了什么?

想要了解到底发生了什么,我们需要看下基准测试下的 popcnt 函数。popcnt 函数是一个叶子函数 - 即该函数没有调用其他任何函数 - 所以编译器可以内联它。

因为该函数是内联函数,编译器可以知道该函数没有任何副作用。popcnt 函数不会影响任何全局变量的状态。因此,调用被消除。下面是编译器看到的:

func BenchmarkPopcnt(b *testing.B) { for i := 0; i < b.N; i++ { //optimised away } }

 

在我测试过的所有版本的 Go 编译器上,仍然会生成循环。 但是英特尔 CPU 确实擅长优化循环,尤其是空循环。

 

练习看汇编

在我们继续之前,让我们看下汇编以确定我们看到的

% go test -gcflags=-S
  • 说明:使用gcflags="-l-S"标识可以禁用内联,那对汇编的输出有什么影响

优化是一件好的事情 值得注意的是,通过消除不必要的计算,使实际代码快速运行的优化与消除没有明显副作用的基准测试的优化是一样的。 随着 Go 编译器的改进,这种情况会越来越普遍

修复基准测试

禁用内联以使基准测试可以正常工作是不现实的。我们想在编译器优化的基础上编译我们的代码。

为了修复这个基准测试,我们必须确保编译器不能证明 BenchmarkPopcnt 的主体不会导致全局状态改变。(注:即让编译器知道 BenchmarkPopcnt 函数有可能会改变全局状态,这样编译器就不用再将函数做内联优化了

package main

import "testing"

const m1 = 0x5555555555555555
const m2 = 0x3333333333333333
const m4 = 0x0f0f0f0f0f0f0f0f
const h01 = 0x0101010101010101

func popcnt(x uint64) uint64 {
   x -= (x >> 1) & m1
   x = (x & m2) + ((x >> 2) & m2)
   x = (x + (x >> 4)) & m4
   return (x * h01) >> 56
}

var Result uint64

func BenchmarkPopcnt(b *testing.B) {
   var r uint64
   for i := 0; i < b.N; i++ {
      r = popcnt(uint64(i))
   }
   Result = r
}

$ go test -bench=. ./
goos: windows
goarch: amd64
pkg: go-test
BenchmarkPopcnt-12      724704199                1.67 ns/op
PASS
ok      go-test 1.641s
 

以上通过增加全局变量 Result 是比较推荐的方式,以此来确保编译器不会对循环主题进行优化。首先,我们把 popcnt 函数的调用结果存储在变量 r 中。其次,因为 r 是局部变量,一旦基准测试结束,变量 r 的生命周期也将结束,所以最后我们把 r 的结果赋值给全局变量 Result。

因为变量 Result 是全局可见,所以编译器不能确定其他导入该包的代码是否也在使用该变量,因此编译器不能对该赋值操作进行优化。

基准测试错误

在基准测试中,for 循环是至关重要的。

这里是两个错误的基准测试,你能解释他们为什么错误吗?

func BenchmarkFibWrong(b *testing.B) {
    Fib(b.N)
}

func BenchmarkFibWrong2(b *testing.B) {
    for n := 0; n < b.N; n++ {
        Fib(n)
    }
}

运行上面的基准测试试一试,你将看到什么?死循环

 

基准测试中使用 math/rand

众所周知,计算机非常擅长预测并缓存(译者注:即 cpu 的局部性原理)。也许我们的 Popcnt 基准测试返回的是一个缓存的结果。让我们看一下下面的例子:

 

众所周知,计算机非常擅长预测并缓存(译者注:即 cpu 的局部性原理)。也许我们的 Popcnt 基准测试返回的是一个缓存的结果。让我们看一下下面的例子:

var Result uint64

func BenchmarkPopcnt(b *testing.B) {
    var r uint64
    for i := 0; i < b.N; i++ {
        r = popcnt(rand.Uint64())
    }

    Result = r
}

以上代码是可靠的吗?如果不是,哪里出错了?

 收集基准测试数据

该 testing 包内置了对生成 CPU,内存和模块配置文件的支持。

  • -cpuprofile=$FILE 收集 CPU 性能分析到 $FILE 文件
  • -memprofile=$FILE,将内存性能分析写入到 $FILE 文件,-memprofilerate=N 调节采样频率为 1/N
  • -blockprofile=$FILE,输出内部 goroutine 阻塞的性能分析文件数据到 $FILE

这些标识也同样可以用于二进制文件

% go test -run=XXX -bench=. -cpuprofile=c.p bytes 
% go tool pprof c.p

benchmark 小结

benchmark 是 go 语言中用于测试性能的一个工具。主要适用于在已知性能瓶颈在哪里时的场景。该测试函数位于_test.go 为结尾的文件中,性能测试函数名以 Benchmark 开头,可以测试出被执行函数被执行的次数,平均每次执行所消耗的时间,以及 cpu 以及内存的性能数据。 同时,在执行基准测试时也需要注意运行环境的稳定性,执行的次数,求得的平均值越准确。

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值