Golang基准测试

447 阅读1分钟

单元测试和基准测试的区别

  • 单元测试:用于进行功能测试
  • 基准测试:用于进行性能测试

单元测试示例

待测试函数如下:

// Fib 计算斐波那契数列中第n个数字
func Fib(n int) int {
   switch n {
   case 0:
      return 0
   case 1:
      return 1
   default:
      return Fib(n-1) + Fib(n-2)
   }
}

进行单元测试:

单元测试函数的名称必须以Test开头

// TestFib 单元测试函数,进行功能测试
func TestFib(t *testing.T) {
   res := Fib(20)
   t.Logf("res:%d\n", res)
}

执行命令go test -v -run=TestFib ./,输出测试结果:

=== RUN   TestFib
    t1_test.go:30: res:6765
--- PASS: TestFib (0.00s)
PASS

基准测试示例

待测试函数如下:

// Fib 计算斐波那契数列中第n个数字
func Fib(n int) int {
   switch n {
   case 0:
      return 0
   case 1:
      return 1
   default:
      return Fib(n-1) + Fib(n-2)
   }
}

进行基准测试:

基准测试函数的名称必须以Benchmark开头

// BenchmarkFib 基准测试函数,进行性能测试
func BenchmarkFib(b *testing.B) {
   b.Logf("BenchmarkFib start, b.N=%d\n", b.N)

   count := 0
   // 会调用 待测试函数 b.N次
   for n := 0; n < b.N; n++ {
      Fib(20)
      count++
   }

   b.Logf("BenchmarkFib end, b.N=%d, count=%d\n", b.N, count)
}

执行go test -v -bench=BenchmarkFib -run=^$ ./-run=^$表示不执行单元测试,防止单元测试的执行影响基准测试的测试结果,输出测试结果:

BenchmarkFib
    t1_test.go:35: BenchmarkFib start, b.N=1
    t1_test.go:44: BenchmarkFib end, b.N=1, count=1
    t1_test.go:35: BenchmarkFib start, b.N=100
    t1_test.go:44: BenchmarkFib end, b.N=100, count=100
    t1_test.go:35: BenchmarkFib start, b.N=10000
    t1_test.go:44: BenchmarkFib end, b.N=10000, count=10000
    t1_test.go:35: BenchmarkFib start, b.N=36450
    t1_test.go:44: BenchmarkFib end, b.N=36450, count=36450
BenchmarkFib-12            36450             32250 ns/op
PASS
ok      go_reflect_1/test_benchmark/t1  2.553s

可以看到,会把最终的b.N(等于36450)测试结果作为基准测试的测试结果。

基准测试详解

  1. go test -v -bench=. ./默认会运行当前包下的所有单元测试,单元测试的执行会影响基准测试的测试结果,可以使用-run=^$使单元测试不执行
  2. b.N是可变化的,当基准测试用例1秒(默认是1s,可用-benchtime修改)内执行完时,b.N的值将递增,然后重新运行基准测试用例,递增规律大概以 1, 2, 3, 5, 10, 20, 30, 50, 100 这样的序列递增,越到后面,增加得越快。

    这里所说的基准测试用例就是基准测试函数,如上文的BenchmarkFib。
    当b.N的值不再递增时,此时b.N的测试结果就是最终基准测试的测试结果。

  3. -benchtime参数,可以是时间3s(3秒),也可以是次数10x(10次)。
  4. -benchmem参数查看内存情况
  5. -count参数用来设置基准测试的轮数
  6. 真正执行测试代码前,可能要做一些准备工作,如构建测试数据,而这部分准备工作不属于性能测试计算范围内,需要排除在外。可以使用ResetTimer方法重置计时,也可以使用StartTimerStopTimer方法来控制何时开始计时、何时结束计时。

实践

StopTimer和StratTimer实践

测试代码如下:

// prepare 模拟被测函数执行前的准备工作
func prepare() {
   time.Sleep(500 * time.Millisecond)
}

// testUnit 是被测函数,执行耗时1s
func testUnit() {
   time.Sleep(1 * time.Second)
}

// BenchmarkTestUnitWithoutStartAndStop 不使用StartTimer和StopTimer
func BenchmarkTestUnitWithoutStartAndStop(b *testing.B) {
   for i := 0; i < b.N; i++ {
      prepare()  // 被测函数执行前的准备工作
      testUnit() // 被测函数
   }
}

// BenchmarkTestUnitWithStartAndStop 使用StartTimer和StopTimer
func BenchmarkTestUnitWithStartAndStop(b *testing.B) {
   for i := 0; i < b.N; i++ {
      b.StopTimer()
      prepare() // 被测函数执行前的准备工作
      b.StartTimer()

      testUnit() // 被测函数
   }
}

可以看到如上有两个基准测试用例:BenchmarkTestUnitWithoutStartAndStopBenchmarkTestUnitWithStartAndStop


使用go test -v -bench=BenchmarkTestUnitWithoutStartAndStop -run=^$ -benchtime=10x -count=3 ./执行BenchmarkTestUnitWithoutStartAndStop测试用例,测试结果:

BenchmarkTestUnitWithoutStartAndStop
BenchmarkTestUnitWithoutStartAndStop-12               10        1505703299 ns/op
BenchmarkTestUnitWithoutStartAndStop-12               10        1503749296 ns/op
BenchmarkTestUnitWithoutStartAndStop-12               10        1503685276 ns/op
PASS
ok      go_reflect_1/test_benchmark/t1  50.188s

从测试结果可以看出:每次执行耗时1.5s左右,也就是说prepare准备工作的耗时也被计算了。


使用go test -v -bench=BenchmarkTestUnitWithStartAndStop -run=^$ -benchtime=10x -count=3 ./执行BenchmarkTestUnitWithStartAndStop测试用例,测试结果:

BenchmarkTestUnitWithStartAndStop
BenchmarkTestUnitWithStartAndStop-12                  10        1002376966 ns/op
BenchmarkTestUnitWithStartAndStop-12                  10        1002312947 ns/op
BenchmarkTestUnitWithStartAndStop-12                  10        1003697554 ns/op
PASS
ok      go_reflect_1/test_benchmark/t1  49.957s

从测试结果可以看出:每次执行耗时1s左右,也就是prepare准备工作的耗时没被计算,证明了StopTimer和StartTimer能够排除「非测试工作」的耗时影响

ResetTimer实践

测试代码如下:

// prepare 模拟被测函数执行前的准备工作
func prepare() {
   time.Sleep(1 * time.Second)
}

// testUnit 是被测函数
func testUnit() {
   time.Sleep(200 * time.Millisecond)
}

func BenchmarkTestUnitWithoutResetTimer(b *testing.B) {
   prepare()

   for i := 0; i < b.N; i++ {
      testUnit()
   }
}

func BenchmarkTestUnitWithResetTimer(b *testing.B) {
   prepare()
   b.ResetTimer()

   for i := 0; i < b.N; i++ {
      testUnit()
   }
}

执行go test -v -bench=BenchmarkTestUnitWithoutResetTimer -run=^$ -count=3 ./,测试结果:

BenchmarkTestUnitWithoutResetTimer
BenchmarkTestUnitWithoutResetTimer-12                  1        1203920272 ns/op
BenchmarkTestUnitWithoutResetTimer-12                  1        1207368966 ns/op
BenchmarkTestUnitWithoutResetTimer-12                  1        1204159098 ns/op
PASS
ok      go_reflect_1/test_benchmark/t1  4.901s

执行go test -v -bench=BenchmarkTestUnitWithResetTimer -run=^$ -count=3 ./,测试结果:

BenchmarkTestUnitWithResetTimer
BenchmarkTestUnitWithResetTimer-12             5         202107783 ns/op
BenchmarkTestUnitWithResetTimer-12             5         203376007 ns/op
BenchmarkTestUnitWithResetTimer-12             5         202894434 ns/op
PASS
ok      go_reflect_1/test_benchmark/t1  15.463s

分析测试结果:
可以看到,不重置计时的话,只会执行1次被测函数,即b.N等于1,因为基准测试用例不能在1s内执行完,b.N不会递增;而重置计时的话,被测函数会执行5次,即b.N等于5,证明了ResetTimer能够排除「非测试工作」的耗时。

探索如何计算ns/op的

基准测试的测试结果一般是这样的: BenchmarkFib-xxx xxxx xxxx ns/op
ns/op是怎么计算的呢?
探索一下...


测试代码1:

func testUnit() {
   time.Sleep(200 * time.Millisecond)
}

func BenchmarkTestNsOp(b *testing.B) {
   time.Sleep(1 * time.Second) // 模拟准备工作耗时

   for i := 0; i < b.N; i++ {
      testUnit()
   }

   time.Sleep(500 * time.Millisecond) // 模拟后置工作耗时
}

执行测试go test -v -bench=BenchmarkTestNsOp -run=^$ ./,测试结果:

BenchmarkTestNsOp
BenchmarkTestNsOp-12                   1        1706702726 ns/op
PASS
ok      go_reflect_1/test_benchmark/t1  2.086s

可以看到执行时间1.7s左右,当b.N等于1时,刚好是BenchmarkTestNsOp函数的执行耗时,因此可给出一个临时结论ns/op = 测试用例执行的耗时 / b.N但是这个结论不大准确,再看看如下的测试2。


测试代码2:

// testUnit 是被测函数
func testUnit() {
   time.Sleep(200 * time.Millisecond)
}

func BenchmarkTestNsOp2(b *testing.B) {
   start := time.Now()

   b.StopTimer()
   time.Sleep(1 * time.Second) // 模拟准备工作耗时
   b.StartTimer()

   testStart := time.Now()
   for i := 0; i < b.N; i++ {
      testUnit()
   }
   testCost := time.Since(testStart)

   b.StopTimer()
   time.Sleep(500 * time.Millisecond) // 模拟后置工作耗时
   b.Logf("BenchmarkTestNsOp2 b.N:%d, test-cost:%v, all-cost:%v\n", b.N, testCost, time.Since(start))
}

执行go test -v -bench=BenchmarkTestNsOp2 -run=^$ ./,测试结果:

BenchmarkTestNsOp2
    t1_test.go:123: BenchmarkTestNsOp2 b.N:1, test-cost:202.811446ms, all-cost:1.706372116s
    t1_test.go:123: BenchmarkTestNsOp2 b.N:4, test-cost:814.561162ms, all-cost:2.320013451s
    t1_test.go:123: BenchmarkTestNsOp2 b.N:5, test-cost:1.016851035s, all-cost:2.523482221s
BenchmarkTestNsOp2-12                  5         203370351 ns/op
PASS
ok      go_reflect_1/test_benchmark/t1  7.507s

分析测试结果:
如果按照临时结论ns/op = 测试用例执行的耗时 / b.N,那此时的结果应该是ns/op = 2.5s / 5 = 0.5s,明显计算错误,因为实际上ns/op是203ms左右

给出最终结论ns/op = 测试用例执行测试的有效耗时 / b.N,这里所说的测试用例执行测试的有效耗时就等于代码中的test-cost,那按照最终结论来计算的话:ns/op = 1.0168s / 5,约等于203ms结果正确!

为什么要强调执行测试的有效耗时
因为测试用例执行时可能执行一些「非测试工作」,而「非测试工作的耗时」会影响测试用例的执行耗时,我们应该关注「测试工作的耗时」StopTimerStartTimer可以排除「非测试工作」的耗时影响。

总结

ns/op = 测试用例执行测试的有效耗时 / b.N