基准测试 Benchmarking


基准测试 实现对测试对象 性能定量 和 可对比的测试
_test.go 结尾的测试文件 如下形式的函数
func BenchmarkXxx(*testing.B)
是基准测试 通过go test命令 加-bench flag执行多个基准测试按顺序运行
func BenchmarkHello(b *testing.B) {
    for i := 0; i < b.N; i++ {
        fmt.Sprintf("hello")
    }
}
基准函数运行目标代码 b.N 次
基准执行期间调整 b.N到基准测试函数持续足够长的时间输出
BenchmarkHello    10000 282 ns/op
循环执行10000次 每次循环花费 282 纳秒(ns)
如果运行前基准测试需耗时的配置 则可先重置定时器
func BenchmarkBigLen(b *testing.B) {
    big := NewBig()
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        big.Len()
    }
}
如基准测试需在并行设置中 测试性能 则可用RunParallel辅助函数
这基准测试一般与go test -cpu 标志一起用
func BenchmarkTemplateParallel(b *testing.B) {
    templ := template.Must(template.New("test").Parse("Hello, {{.}}!"))
    b.RunParallel(func(pb *testing.PB) { // 每个 goroutine有自己的bytes.Buffer
        var buf bytes.Buffer
        for pb.Next() { //所有goroutine一起 循环共执行 b.N 次
            buf.Reset()
            templ.Execute(&buf, "World")
        }
    })
}

基准测试示例

Fib 进行基准测试:
func BenchmarkFib10(b *testing.B) {
        for n := 0; n < b.N; n++ {
                Fib(10)
        }
}
$ go test -bench=.

测试Fib(10)可能需要测试更多不同的情况 改写测试代码
func BenchmarkFib1(b *testing.B)  { benchmarkFib(1, b) }
func BenchmarkFib2(b *testing.B)  { benchmarkFib(2, b) }
func BenchmarkFib3(b *testing.B)  { benchmarkFib(3, b) }
func BenchmarkFib10(b *testing.B) { benchmarkFib(10, b) }
func BenchmarkFib20(b *testing.B) { benchmarkFib(20, b) }
func BenchmarkFib40(b *testing.B) { benchmarkFib(40, b) }
func benchmarkFib(i int, b *testing.B) {
    for n := 0; n < b.N; n++ {
        Fib(i)
    }
}
$ go test -bench=.
默认基准测试最少运行 1 秒
如果基准测试函数返回时 还不到 1 秒钟 b.N 的值会按照序列 1,2,5,10,20,50,.. 增加
再次运行基准测测试函数
为了更精确的结果 可通过增加标志 -benchtime 运行更多次
$ go test -bench=Fib40 -benchtime=20s

B 类型

B是传递给基准测试函数的类型 用于管理基准测试的计时行为 指示应该迭代运行测试多少次
基准测试在基准测试函数返回时 或在基准测试函数调用 FailNow Fatal Fatalf SkipNow Skip 或Skipf 任意一方法时
测试即宣告结束 至于其他报告方法 如 Log 和 Error 的变种 则可在其他 goroutine同时进行调用
基准测试在执行过程中积累日志
在测试完毕时将日志转储到标准错误
但跟单元测试不一样 为避免基准测试的结果受到日志打印操作的影响
基准测试总是会把日志打印出来
B 类型报告方法使用方式和 T 类型一样
基准测试中不需要用 主要是测性能
对 B 类型中其他的一些方法进行讲解

计时方法

有三个方法计时
StartTimer 开始对测试进行计时 该方法在基准测试开始时自动被调用 可在调用 StopTimer后恢复计时
StopTimer 停止对测试进行计时 当需要执行复杂初始化操作 且不想对这些操作进行测量时 就可用来暂时停止计时
ResetTimer 对已经逝去的基准测试时间及内存分配计数器清零 对正在运行中的计时器不会产生效果

并行执行

RunParallel 方法以并行方式执行给定的基准测试
RunParallel 创建出多个goroutine将 b.N分配给这些 goroutine 执行
goroutine数量的默认值为 GOMAXPROCS 用户
如果要增加非CPU受限(non-CPU-bound)基准测试的并行性
可以在 RunParallel 前调用 SetParallelism(如 SetParallelism(2) 则 goroutine 数量为 2*GOMAXPROCS)
RunParallel 通常会与 -cpu 标志一同使用
body 函数将在每个 goroutine 中执行 这个函数需要设置所有 goroutine 本地的状态
并迭代直到 pb.Next 返回 false 值为止 因为 StartTimer、StopTime 和 ResetTimer
这三个方法都带有全局作用 所以body 函数不应该调用这些方法
除此之外 body 函数也不应该调用 Run 方法

内存统计

ReportAllocs 方法打开当前基准测试的内存统计
与go test用-benchmem 标志类似
但 ReportAllocs 只影响那些调用了该函数的基准测试
func BenchmarkTmplExucte(b *testing.B) {
    b.ReportAllocs()
    templ := template.Must(template.New("test").Parse("Hello, {{.}}!"))
    b.RunParallel(func(pb *testing.PB) {   // Each goroutine has its own bytes.Buffer.
        var buf bytes.Buffer
        for pb.Next() { // The loop body is executed b.N times total across all goroutines.
            buf.Reset()
            templ.Execute(&buf, "World")
        }
    })
}

基准测试结果

testing 包中BenchmarkResult 类型 提供帮助  保存基准测试的结果
type BenchmarkResult struct {
    N         int           // The number of iterations 即 b.N
    T         time.Duration // The total time taken 基准测试花费的时间
    Bytes     int64         // Bytes processed in one iteration 一次迭代处理的字节数 通过 b.SetBytes 设置
    MemAllocs uint64        // The total number of memory allocations 总的分配内存的次数
    MemBytes  uint64        // The total number of bytes allocated 总的分配内存的字节数
}
该类型还提供相应的计算每个操作每秒相应指标的方法
package main
import (
    "bytes"
    "fmt"
    "testing"
    "text/template"
)
func main() {
    benchmarkResult := testing.Benchmark(func(b *testing.B) {
        templ := template.Must(template.New("test").Parse("Hello, {{.}}!"))
        // RunParallel will create GOMAXPROCS goroutines and distribute work among them.
        b.RunParallel(func(pb *testing.PB) { // Each goroutine has its own bytes.Buffer.
            var buf bytes.Buffer
            for pb.Next() {// The loop body is executed b.N times total across all goroutines.
                buf.Reset()
                templ.Execute(&buf, "World")
            }
        })
    })
    // fmt.Printf("%8d\t%10d ns/op\t%10d B/op\t%10d allocs/op\n", benchmarkResult.N, benchmarkResult.NsPerOp(), benchmarkResult.AllocedBytesPerOp(), benchmarkResult.AllocsPerOp())
    fmt.Printf("%s\t%s\n", benchmarkResult.String(), benchmarkResult.MemString())
}