返回介绍

上卷 程序设计

中卷 标准库

下卷 运行时

源码剖析

附录

12.2 基准测试

发布于 2024-10-12 19:15:50 字数 5518 浏览 0 评论 0 收藏 0

有时也称作性能测试。目的是获知算法执行时间,及内存开销。

  • 保存在 _test.go 文件中。
  • 函数以 Benchmark 为前缀,
  • 类型 BT 方法类似,省略。
  • go test -bench 执行。
  • 仅执行性能测试,可用 -run NONE 忽略单元测试。
func BenchmarkAdd(b *testing.B) {
    for i := 0; i < b.N; i++ {
        add(1, 2)
    }
}
$ go test -v -bench . -run None ./mylib

goos: linux
goarch: amd64
pkg: test/mylib
cpu: Intel(R) Core(TM) i5-5257U CPU @ 2.70GHz

BenchmarkAdd
BenchmarkAdd-2          1000000000               0.3534 ns/op

PASS
ok      test/mylib      0.417s
  • -bench :指定要运行的测试。(正则表达式, -bench .
  • -benchtime :单次测试运行时间或循环次数。(默认 1s,1m20s,100x)
  • -count :执行几轮测试。(benchtime * count)
  • -cpu :测试所用 CPU 核心数。( -cpu 1,2,4 执行三轮测试)
  • -list : 列出测试函数,不执行。
  • -benchmem :显示内存分配(堆)信息。
$ go test -v -run None -bench Add -count 2 ./mylib

BenchmarkAdd-2          1000000000               0.3591 ns/op
BenchmarkAdd-2          1000000000               0.3588 ns/op
$ go test -v -run None -bench Add -cpu 1,2,4 ./mylib

BenchmarkAdd            1000000000               0.3583 ns/op
BenchmarkAdd-2          1000000000               0.3594 ns/op
BenchmarkAdd-4          1000000000               0.3552 ns/op

如果执行次数足够多,则 benchtime 设置的时长无效。
对于某些耗时的目标,设置足够长的时间或次数,以便有足够取样获取平均值。

func BenchmarkSleep(b *testing.B) {
 	for i := 0; i < b.N; i++ {
 		time.Sleep(time.Second)
 	}
}
$ go test -v -run None -bench Add -benchtime 10s ./mylib

BenchmarkAdd
BenchmarkAdd-2          1000000000               0.3609 ns/op
PASS
ok      test/mylib      0.440s


$ go test -v -run None -bench Sleep -benchtime 10s ./mylib

BenchmarkSleep
BenchmarkSleep-2              10        1003276590 ns/op

PASS
ok      test/mylib      11.048s

内部实现

内部通过增加循环次数,直到取样(时间或次数上限)足够,以获得最佳平均值。

func BenchmarkAdd(b *testing.B) {
	println("b.N = ", b.N)

	for i := 0; i < b.N; i++ {
		add(1, 2)
	}
}
$ go test -v -run None -bench Add ./mylib

BenchmarkAdd
b.N =  1
b.N =  100
b.N =  10000
b.N =  1000000
b.N =  100000000
b.N =  1000000000

BenchmarkAdd-2          1000000000               0.3560 ns/op

PASS
ok      test/mylib      0.425s

决定循环次数( b.N )的因素,按优先级次序:

  • 手工指定次数( -benchtime 10x )。
  • 内部次数上限( 1e9 , 1000000000 )。
  • 手工指定时长( -benchtime 10s )。

另外,性能测试会执行 runtime.GC 清理现场,以确保测试结果不受干扰。

// benchmark.go

func (b *B) run() {
	b.doBench()
}

func (b *B) doBench() BenchmarkResult {
	go b.launch()
	<-b.signal
	return b.result
}

func (b *B) launch() {

    defer func() {
		b.signal <- true
	}()

	// 指定次数。
	if b.benchTime.n > 0 {
		if b.benchTime.n > 1 {
			b.runN(b.benchTime.n)
		}
	} else {
        // 指定时长,默认 1 秒。
		d := b.benchTime.d
        
        // 时间不够,且次数没有超出上限,增加循环次数重来。
        // 提示,b.duration 由 StopTimer 更新。
		for n := int64(1); !b.failed && b.duration < d && n < 1e9; {
			last := n
            
			// 重新计算循环次数。
            n = goalns * prevIters / prevns
            n += n / 5
            n = min(n, 100*last)
            n = max(n, last+1)
            ...
            
            // 次数上限。
			n = min(n, 1e9)

			b.runN(int(n))
		}
	}
    
	b.result = BenchmarkResult{b.N, b.duration, ...}
}

func (b *B) runN(n int) {

    // Try to get a comparable environment for each run
	// by clearing garbage from previous runs.
	runtime.GC()
    
	b.ResetTimer()
	b.StartTimer()
    
	b.benchFunc(b)   // 测试函数。
    
	b.StopTimer()
}
// benchmark.go

func (b *B) StartTimer() {
	b.start = time.Now()
}

func (b *B) StopTimer() {
	b.duration += time.Since(b.start)
}
// benchmark.go

type BenchmarkResult struct {
	N         int           // The number of iterations.
	T         time.Duration // The total time taken.
}

func (r BenchmarkResult) NsPerOp() int64 {
	return r.T.Nanoseconds() / int64(r.N)
}

子测试

操作与 T 基本一致。但没有 Parallel ,而是 RunParallel (详见后文)。

每次执行都会调用 runtime.GC 清理现场,以减少外部干扰,更别说多个子测试并发了。

func BenchmarkSubs(b *testing.B) {
	b.Log("setup")
	b.Cleanup(func(){ b.Log("cleanup") })

	b.Run("A", BenchmarkA)
	b.Run("B", BenchmarkB)
	b.Run("C", BenchmarkC)
}
$ go test -v -run None -bench Subs ./mylib

BenchmarkSubs

    add_test.go:33: setup
    
BenchmarkSubs/A
BenchmarkSubs/A-2              1        1000442000 ns/op
BenchmarkSubs/B
BenchmarkSubs/B-2              1        1002897400 ns/op
BenchmarkSubs/C
BenchmarkSubs/C-2              1        1003405600 ns/op

    add_test.go:34: cleanup
    
PASS
ok      test/mylib      3.036s


$ go test -v -run None -bench Subs/B ./mylib

BenchmarkSubs

    add_test.go:33: setup
    
BenchmarkSubs/B
BenchmarkSubs/B-2              1        1001120200 ns/op

    add_test.go:34: cleanup
    
PASS
ok      test/mylib      1.029s

计时器

计时器默认自动处理。如测试逻辑中有需要排除的因素,可手工调用。

func BenchmarkTimer(b *testing.B) {

	// setup
	time.Sleep(time.Second)

	// teardown
	defer time.Sleep(time.Second)

	// 重置计时器,避免 setup 干扰。
	b.ResetTimer()

	for i := 0; i < b.N; i++ {
		add(1, 2)
	}

	// 停止计时器,避免 teardown 干扰。
	b.StopTimer()
}

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文