背景
中介绍如何进行单元测试,可以帮助我们看写的对不对,如果对性能有要求,还要看单元好不好,这时候就需要基准测试了。
通过基准测试我们得到:
-
函数的执行耗时是否符合预期
-
内存占用、内存分配次数是否符合预期
-
cpu 消耗是否否和预期
-
...
基本用法
基本格式
-
benchmark 和普通的单元测试用例一样,都位于 _test.go 文件中。
-
函数名以 Benchmark 开头,参数是 b *testing.B。和普通的单元测试用例很像,单元测试函数名以 Test 开头,参数是 t *testing.T
-
go test 命令默认不运行 benchmark 用例的,如果我们想运行 benchmark 用例,则需要加上 -bench 参数
命令行参数
先看一个复杂一些的命令,再逐步讲解每个参数的含义
go test -bench=BenchmarkIsInListReflect -benchmem -count=2 -cpu=2,8,12,20 -benchtime=5s -cpuprofile=cpu.out -memprofile=cpu.out
参数 | 含义 |
---|---|
-bench regexp | 性能测试,支持表达式对测试函数进行筛选。可以用 . 表示所有的函数 |
-benchmem | 性能测试的时候显示测试函数的内存分配的统计信息 |
-count n | 运行测试和性能多少次,默认一次 |
-cpu | 执行测试的cpu 核数,默认为 GOMAXPROCS,支持传入一个列表作为参数,如:-cpu=2,4 |
-benchtime | benchmark 的默认时间是 1s,可以通 -benchtime 来改变测试时间。 |
-cpuprofile | 输出cpu性能文件 |
-memprofile | 输出mem内存性能文件 |
-run regexp | 只运行特定的测试函数, 比如-run ABC只测试函数名中包含ABC的测试函数 |
-v | 显示测试的详细信息,也会把Log、Logf方法的日志显示出来 |
testing.B 方法&属性
先看一个简单的基准测试的写法
func IsInList(value int, list []int) bool {
for i := 0; i < len(list); i++ {
if list[i] == value {
return true
}
}
return false
}
func IsInListReflect(elem interface{}, list interface{}) bool {
v := reflect.ValueOf(list)
for i := 0; i < v.Len(); i++ {
if v.Index(i).Interface() == elem {
return true
}
}
return false
}
// ------------------------------------------------------
func BenchmarkIsInListNormal(b *testing.B) {
n := b.N
fmt.Printf("BenchmarkIsInListNormal: ========== %dn", n)
for i := 0; i < n; i++ {
IsInList(list_value, list)
}
}
func BenchmarkIsInListReflect(b *testing.B) {
n := b.N
fmt.Printf("BenchmarkIsInListReflect: ========== %dn", n)
for i := 0; i < b.N; i++ {
IsInListReflect(list_value, list)
}
}
testing.B 的方法或者属性 | 含义 |
---|---|
b.N | b.N 表示这个用例需要运行的次数。b.N 对于每个用例都是不一样的。b.N 从 1 开始,如果该用例能够在 1s 内完成,b.N 的值便会增加,再次执行。 |
ResetTimer() | 如果在 benchmark 开始前,需要一些准备工作,如果准备工作比较耗时,则需要将这部分代码的耗时忽略掉。在耗时的步骤结束后调用此方法。 |
StopTimer() | 分别为停止计时和开始计时,如果在一个基准测试中有些步骤的顺序要除外,如数据准备等,可以通过这两个函数配合完成。 |
StartTimer() | |
ReportAllocs() | 它相当于设置-benchmem,但它只影响调用ReportAllocs的基准函数。 |
RunParallel(body func(*PB)) | RunParallel 并行运行基准测试。它创建多个 goroutine 并在它们之间分配 b.N 次迭代。Goroutine 的数量默认为 GOMAXPROCS。要增加非 CPU 限制基准测试的并行性,请在 RunParallel 之前调用 SetParallelism。RunParallel 通常与 go test -cpu 标志一起使用。主体函数将在每个 goroutine 中运行。它应该设置任何 goroutine-local 状态,然后迭代直到 pb.Next 返回 false。它不应该使用 StartTimer、StopTimer 或 ResetTimer 函数,因为它们具有全局作用。它也不应该调用 Run。 |
结果输出
运行下上面的测试看看结果输出
go test -bench=BenchmarkIsInListReflect -benchmem
对上面的结果逐行分析
testing.B 的方法或者属性 | 含义 |
---|---|
BenchmarkIsInListReflect: ========== 1(100、10000、39852) | 基准的函数的日志,此处打印了 b.N ,可以清楚看到 b.N 的递增过程 |
BenchmarkIsInListReflect-12 | BenchmarkIsInListReflect 是测试的函数名 -12 表示GOMAXPROCS(线程数)的值为12 |
39852 | 表示一共执行了39852次,即b.N的值 |
29603 ns/op | 表示平均每次操作花费了29603 纳秒 |
8024 B/op | 表示每次申请了 8024 Byte 内存 |
1001 allocs/op | 表示每次操作申请了 1001 次内存 |
原理
以单个Benchmark举例串起流程分析下原理
如上图,浅蓝色部分就是开发者自行编写的benchmark方法,调用逻辑按箭头方向依次递进。
B.run1()的作用是先尝试跑一次,在这次尝试中要做 竟态检查 和 当前benchmark是否被skip了。目的检查当前benchmark是否有必要继续执行。
go test 命令有-cpu参数,用于控制benchmark分别在不同的P数量下执行。这里就对应上图绿色部分,每次通过runtime.GOMAXPROCS(n)更新P个数,然后调用B.doBench()。
核心方法是红色部分的B.runN(n)。形参n值就是b.N值,由外部传进。n不断被逼近上限,逼近策略不能过快,过快可能引起benchmark执行超时。
橙色部分就是逼近策略。先通过n/=int(nsop)来估算b.N的上限,然后再通过n=max(min(n+n/5, 100*last), last+1)计算最后的b.N。benchmark可能是CPU型或IO型,若直接使用第一次估算的b.N值会过于粗暴,可能使结果不准确,所以需要做进一步的约束来逼近。
场景演示
学习了基准测试基本用法和原理后,肯定已经跃跃欲试了,下面构造一些场景小试牛刀。
下面的代码收录在 code.byted.org/gaoweizong/…
对比同一个功能的不同实现
需求:把 int 转为 string 。
实现:
func Int2StringFmt(n int) string {
return fmt.Sprintf("%d", n)
}
func Int2StringFormat(n int) string {
return strconv.FormatInt(int64(n), 10)
}
func Int2StringItoa(n int) string {
return strconv.Itoa(n)
}
// --------------------------------------------
func BenchmarkInt2StringFmt(b *testing.B) {
num := 73847363
for i := 0; i < b.N; i++ {
Int2StringFmt(num)
}
}
func BenchmarkInt2StringItoa(b *testing.B) {
num := 73847363
for i := 0; i < b.N; i++ {
Int2StringItoa(num)
}
}
func BenchmarkInt2StringFormat(b *testing.B) {
num := 73847363
for i := 0; i < b.N; i++ {
Int2StringFormat(num)
}
}
执行一下看看 :go test -bench=BenchmarkInt2String -benchmem -count 5
-
通过 -count 指定执行 5次,避免单次执行差异导致结果不准确,每个函数的执行结果还是比较稳定
-
通过 结果可以看出来 BenchmarkInt2StringFmt 比 其他两个慢了 3 倍左右。从内存分配及分配次数可以看出来,BenchmarkInt2StringFmt 是其他的两个倍。
-
BenchmarkInt2StringItoa 和 BenchmarkInt2StringFormat 性能几乎一样。查看源码可以发现,Itoa 的实现就是调用了 FormatInt ,符合预期。
func FormatInt(i int64, base int) string {
if fastSmalls && 0