我们在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 版本之前,基准测试的迭代次数四舍五入为 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.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)
}
}
运行上面的基准测试试一试,你将看到什么?死循环
众所周知,计算机非常擅长预测并缓存(译者注:即 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,内存和模块配置文件的支持。
这些标识也同样可以用于二进制文件
% go test -run=XXX -bench=. -cpuprofile=c.p bytes % go tool pprof c.p
benchmark 是 go 语言中用于测试性能的一个工具。主要适用于在已知性能瓶颈在哪里时的场景。该测试函数位于_test.go 为结尾的文件中,性能测试函数名以 Benchmark 开头,可以测试出被执行函数被执行的次数,平均每次执行所消耗的时间,以及 cpu 以及内存的性能数据。 同时,在执行基准测试时也需要注意运行环境的稳定性,执行的次数,求得的平均值越准确。