基准测试分享 golang

2023年 8月 14日 84.0k 0

背景

中介绍如何进行单元测试,可以帮助我们看写的对不对,如果对性能有要求,还要看单元好不好,这时候就需要基准测试了。

通过基准测试我们得到:

  • 函数的执行耗时是否符合预期

  • 内存占用、内存分配次数是否符合预期

  • 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

image

对上面的结果逐行分析

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举例串起流程分析下原理

image

如上图,浅蓝色部分就是开发者自行编写的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/…

对比同一个功能的不同实现

image

需求:把 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

image

  • 通过 -count 指定执行 5次,避免单次执行差异导致结果不准确,每个函数的执行结果还是比较稳定

  • 通过 结果可以看出来 BenchmarkInt2StringFmt 比 其他两个慢了 3 倍左右。从内存分配及分配次数可以看出来,BenchmarkInt2StringFmt 是其他的两个倍。

  • BenchmarkInt2StringItoa 和 BenchmarkInt2StringFormat 性能几乎一样。查看源码可以发现,Itoa 的实现就是调用了 FormatInt ,符合预期。

func FormatInt(i int64, base int) string {
if fastSmalls && 0

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论