关于golang:benchmark-基准测试

4次阅读

共计 6573 个字符,预计需要花费 17 分钟才能阅读完成。

转载自:benchmark 基准测试

1 稳固的测试环境

当咱们尝试去优化代码的性能时,首先得晓得以后的性能怎么样。Go 语言规范库内置的 testing 测试框架提供了基准测试 (benchmark) 的能力,能让咱们很容易地对某一段代码进行性能测试。

性能测试受环境的影响很大,为了保障测试的可重复性,在进行性能测试时,尽可能地放弃测试环境的稳固。

  • 机器处于闲置状态,测试时不要执行其余工作,也不要和其他人共享硬件资源。
  • 机器是否敞开了节能模式,个别笔记本会默认关上这个模式,测试时敞开。
  • 防止应用虚拟机和云主机进行测试,个别状况下,为了尽可能地进步资源的利用率,虚拟机和云主机 CPU 和内存个别会超调配,超分机器的性能体现会十分地不稳固。

超调配是针对硬件资源来说的,商业上对应的就是云主机的超卖。虚拟化技术带来的最大间接收益是服务器整合,通过 CPU、内存、存储、网络的超调配(Overcommitment)技术,最大化服务器的使用率。例如,虚拟化的技能之一就是得心应手的操控 CPU,例如一台 32U(物理外围)的服务器可能会创立出 128 个 1U(虚构外围)的虚拟机,当物理服务器资源闲置时,CPU 超调配个别不会对虚拟机上的业务产生显著影响,但如果大部分虚拟机都处于忙碌状态时,那么各个虚拟机为了取得物理服务器的资源就要相互竞争,互相期待。Linux 上专门有一个指标,Steal Time(st),用来掂量被虚拟机监视器 (Hypervisor) 偷去给其它虚拟机应用的 CPU 工夫所占的比例。

2 benchmark 的应用

2.1 一个简略的例子

Go 语言规范库内置了反对 benchmark 的 testing 库,接下来看一个简略的例子:

应用 go mod init example 初始化一个模块,新增 fib.go 文件,实现函数 fib,用于计算第 N 个菲波那切数。

// fib.go
package main

func fib(n int) int {
    if n == 0 || n == 1 {return n}
    return fib(n-2) + fib(n-1)
}

接下来,咱们在 fib_test.go 中实现一个 benchmark 用例:

// fib_test.go
package main

import "testing"

func BenchmarkFib(b *testing.B) {
    for n := 0; n < b.N; n++ {fib(30) // run fib(30) b.N times
    }
}
  • benchmark 和一般的单元测试用例一样,都位于 _test.go 文件中。
  • 函数名以 Benchmark 结尾,参数是 b *testing.B。和一般的单元测试用例很像,单元测试函数名以 Test 结尾,参数是 t *testing.T

2.2 运行用例

go test <module name>/<package name> 用来运行某个 package 内的所有测试用例。

  • 运行以后 package 内的用例:go test examplego test .
  • 运行子 package 内的用例:go test example/<package name>go test ./<package name>
  • 如果想递归测试当前目录下的所有的 package:go test ./...go test example/...

go test 命令默认不运行 benchmark 用例的,如果咱们想运行 benchmark 用例,则须要加上 -bench 参数。例如:

$ go test -bench .
goos: darwin
goarch: amd64
pkg: example
BenchmarkFib-8               200           5865240 ns/op
PASS
ok      example 1.782s

-bench 参数反对传入一个正则表达式,匹配到的用例才会失去执行,例如,只运行以 Fib 结尾的 benchmark 用例:

$ go test -bench='Fib$' .
goos: darwin
goarch: amd64
pkg: example
BenchmarkFib-8               202           5980669 ns/op
PASS
ok      example 1.813s

2.3 benchmark 是如何工作的

benchmark 用例的参数 b *testing.B,有个属性 b.N 示意这个用例须要运行的次数。b.N 对于每个用例都是不一样的。

那这个值是如何决定的呢?b.N 从 1 开始,如果该用例可能在 1s 内实现,b.N 的值便会减少,再次执行。b.N 的值大略以 1, 2, 3, 5, 10, 20, 30, 50, 100 这样的序列递增,越到前面,减少得越快。咱们仔细观察上述例子的输入:

BenchmarkFib-8               202           5980669 ns/op

BenchmarkFib-8 中的 -8GOMAXPROCS,默认等于 CPU 核数。能够通过 -cpu 参数扭转 GOMAXPROCS-cpu 反对传入一个列表作为参数,例如:

$ go test -bench='Fib$' -cpu=2,4 .
goos: darwin
goarch: amd64
pkg: example
BenchmarkFib-2               206           5774888 ns/op
BenchmarkFib-4               205           5799426 ns/op
PASS
ok      example 3.563s

在这个例子中,扭转 CPU 的核数对后果简直没有影响,因为这个 Fib 的调用是串行的。

202 和 5980669 ns/op 示意用例执行了 202 次,每次破费约 0.006s。总耗时比 1s 略多。

2.4 晋升准确度

对于性能测试来说,晋升测试准确度的一个重要伎俩就是减少测试的次数。咱们能够应用 -benchtime 和 -count 两个参数达到这个目标。

benchmark 的默认工夫是 1s,那么咱们能够应用 -benchtime 指定为 5s。例如:

$ go test -bench='Fib$' -benchtime=5s .
goos: darwin
goarch: amd64
pkg: example
BenchmarkFib-8              1033           5769818 ns/op
PASS
ok      example 6.554s

理论执行的工夫是 6.5s,比 benchtime 的 5s 要长,测试用例编译、执行、销毁等是须要工夫的。

将 -benchtime 设置为 5s,用例执行次数也变成了原来的 5 倍,每次函数调用工夫仍为 0.6s,简直没有变动。

-benchtime 的值除了是工夫外,还能够是具体的次数。例如,执行 30 次能够用 -benchtime=30x

$ go test -bench='Fib$' -benchtime=50x .
goos: darwin
goarch: amd64
pkg: example
BenchmarkFib-8                50           6121066 ns/op
PASS
ok      example 0.319s

调用 50 次 fib(30),仅破费了 0.319s。

-count 参数能够用来设置 benchmark 的轮数。例如,进行 3 轮 benchmark。

$ go test -bench='Fib$' -benchtime=5s -count=3 .
goos: darwin
goarch: amd64
pkg: example
BenchmarkFib-8               975           5946624 ns/op
BenchmarkFib-8              1023           5820582 ns/op
BenchmarkFib-8               961           6096816 ns/op
PASS
ok      example 19.463s

2.5 内存分配情况

-benchmem 参数能够度量内存调配的次数。内存调配次数也性能也是非亲非故的,例如不合理的切片容量,将导致内存重新分配,带来不必要的开销。

在上面的例子中,generateWithCap 和 generate 的作用是统一的,生成一组长度为 n 的随机序列。惟一的不同在于,generateWithCap 创立切片时,将切片的容量 (capacity) 设置为 n,这样切片就会一次性申请 n 个整数所需的内存。

// generate_test.go
package main

import (
    "math/rand"
    "testing"
    "time"
)

func generateWithCap(n int) []int {rand.Seed(time.Now().UnixNano())
    nums := make([]int, 0, n)
    for i := 0; i < n; i++ {nums = append(nums, rand.Int())
    }
    return nums
}

func generate(n int) []int {rand.Seed(time.Now().UnixNano())
    nums := make([]int, 0)
    for i := 0; i < n; i++ {nums = append(nums, rand.Int())
    }
    return nums
}

func BenchmarkGenerateWithCap(b *testing.B) {
    for n := 0; n < b.N; n++ {generateWithCap(1000000)
    }
}

func BenchmarkGenerate(b *testing.B) {
    for n := 0; n < b.N; n++ {generate(1000000)
    }
}

运行该用例的后果是:

go test -bench='Generate' .
goos: darwin
goarch: amd64
pkg: example
BenchmarkGenerateWithCap-8            44          24294582 ns/op
BenchmarkGenerate-8                   34          30342763 ns/op
PASS
ok      example 2.171s

能够看到生成 100w 个数字的随机序列,GenerateWithCap 的耗时比 Generate 少 20%。

咱们能够应用 -benchmem 参数看到内存调配的状况:

goos: darwin
goarch: amd64
pkg: example
BenchmarkGenerateWithCap-8  43  24335658 ns/op  8003641 B/op    1 allocs/op
BenchmarkGenerate-8         33  30403687 ns/op  45188395 B/op  40 allocs/op
PASS
ok      example 2.121s

Generate 调配的内存是 GenerateWithCap 的 6 倍,设置了切片容量,内存只调配一次,而不设置切片容量,内存调配了 40 次。

2.6 测试不同的输出

不同的函数复杂度不同,O(1),O(n),O(n^2) 等,利用 benchmark 验证复杂度一个简略的形式,是结构不同的输出。对方才的 benchmark 稍作革新,便可能达到目标。

// generate_test.go
package main

import (
    "math/rand"
    "testing"
    "time"
)

func generate(n int) []int {rand.Seed(time.Now().UnixNano())
    nums := make([]int, 0)
    for i := 0; i < n; i++ {nums = append(nums, rand.Int())
    }
    return nums
}
func benchmarkGenerate(i int, b *testing.B) {
    for n := 0; n < b.N; n++ {generate(i)
    }
}

func BenchmarkGenerate1000(b *testing.B)    {benchmarkGenerate(1000, b) }
func BenchmarkGenerate10000(b *testing.B)   {benchmarkGenerate(10000, b) }
func BenchmarkGenerate100000(b *testing.B)  {benchmarkGenerate(100000, b) }
func BenchmarkGenerate1000000(b *testing.B) {benchmarkGenerate(1000000, b) }

这里,咱们实现一个辅助函数 benchmarkGenerate 容许传入参数 i,并结构了 4 个不同输出的 benchmark 用例。运行后果如下:

$ go test -bench .                                                       
goos: darwin
goarch: amd64
pkg: example
BenchmarkGenerate1000-8            34048             34643 ns/op
BenchmarkGenerate10000-8            4070            295642 ns/op
BenchmarkGenerate100000-8            403           3230415 ns/op
BenchmarkGenerate1000000-8            39          32083701 ns/op
PASS
ok      example 6.597s

通过测试后果能够发现,输出变为原来的 10 倍,函数每次调用的时长也差不多是原来的 10 倍,这阐明复杂度是线性的。

3 benchmark 注意事项

3.1 ResetTimer

如果在 benchmark 开始前,须要一些筹备工作,如果筹备工作比拟耗时,则须要将这部分代码的耗时疏忽掉。比方上面的例子:

func BenchmarkFib(b *testing.B) {time.Sleep(time.Second * 3) // 模仿耗时筹备工作
    for n := 0; n < b.N; n++ {fib(30) // run fib(30) b.N times
    }
}

运行后果是:

$ go test -bench='Fib$' -benchtime=50x .
goos: darwin
goarch: amd64
pkg: example
BenchmarkFib-8                50          65912552 ns/op
PASS
ok      example 6.319s

50 次调用,每次调用约 0.66s,是之前的 0.06s 的 11 倍。究其原因,受到了耗时筹备工作的烦扰。咱们须要用 ResetTimer 屏蔽掉:

func BenchmarkFib(b *testing.B) {time.Sleep(time.Second * 3) // 模仿耗时筹备工作
    b.ResetTimer() // 重置定时器
    for n := 0; n < b.N; n++ {fib(30) // run fib(30) b.N times
    }
}

运行后果恢复正常,每次调用约 0.06s。

$ go test -bench='Fib$' -benchtime=50x .
goos: darwin
goarch: amd64
pkg: example
BenchmarkFib-8                50           6187485 ns/op
PASS
ok      example 6.330s

3.2 StopTimer & StartTimer

还有一种状况,每次函数调用前后须要一些筹备工作和清理工作,咱们能够应用 StopTimer 暂停计时以及应用 StartTimer 开始计时。

例如,如果测试一个冒泡函数的性能,每次调用冒泡函数前,须要随机生成一个数字序列,这是十分耗时的操作,这种场景下,就须要应用 StopTimer 和 StartTimer 防止将这部分工夫计算在内。

例如:

// sort_test.go
package main

import (
    "math/rand"
    "testing"
    "time"
)

func generateWithCap(n int) []int {rand.Seed(time.Now().UnixNano())
    nums := make([]int, 0, n)
    for i := 0; i < n; i++ {nums = append(nums, rand.Int())
    }
    return nums
}

func bubbleSort(nums []int) {for i := 0; i < len(nums); i++ {for j := 1; j < len(nums)-i; j++ {if nums[j] < nums[j-1] {nums[j], nums[j-1] = nums[j-1], nums[j]
            }
        }
    }
}

func BenchmarkBubbleSort(b *testing.B) {
    for n := 0; n < b.N; n++ {b.StopTimer()
        nums := generateWithCap(10000)
        b.StartTimer()
        bubbleSort(nums)
    }
}

执行该用例,每次排序耗时约 0.1s。

$ go test -bench='Sort$' .
goos: darwin
goarch: amd64
pkg: example
BenchmarkBubbleSort-8                  9         113280509 ns/op
PASS
ok      example 1.146s
正文完
 0