跳转到主要内容

这篇文章继续了我几周前开始的测试包系列。您可以在此处阅读上一篇关于编写表驱动测试的文章。您可以在 https://github.com/davecheney/fib 存储库中找到下面提到的代码。

介绍


Go 测试包包含一个基准测试工具,可用于检查 Go 代码的性能。这篇文章解释了如何使用测试包来编写一个简单的基准测试。

您还应该查看 Profiling Go 程序的介绍性段落,特别是有关在您的机器上配置电源管理的部分。无论好坏,现代 CPU 严重依赖主动热管理,这会给基准测试结果增加噪音。

编写基准


我们将重用上一篇文章中的 Fib 函数。

func Fib(n int) int {
        if n < 2 {
                return n
        }
        return Fib(n-1) + Fib(n-2)
}


基准放置在 _test.go 文件中,并遵循其测试对应物的规则。在第一个示例中,我们将对计算斐波那契数列中第 10 个数字的速度进行基准测试。

// from fib_test.go
func BenchmarkFib10(b *testing.B) {
        // run the Fib function b.N times
        for n := 0; n < b.N; n++ {
                Fib(10)
        }
}


编写基准测试与编写测试非常相似,因为它们共享测试包中的基础设施。一些关键的区别是

  • 基准函数以 Benchmark 而非 Test 开头。
  • 基准测试函数由测试包运行多次。 b.N 的值将每次增加,直到基准运行者对基准的稳定性感到满意为止。这有一些重要的影响,我们将在本文后面进行研究。
  • 每个基准测试必须执行被测代码 b.N 次。 BenchmarkFib10 中的 for 循环将出现在每个基准函数中。


运行基准


现在我们在 fib 包的测试中定义了一个基准函数,我们可以使用 go test -bench= 调用它。

% go test -bench=.
PASS
BenchmarkFib10   5000000               509 ns/op
ok      github.com/davecheney/fib       3.084s


分解上面的文本,我们传递 -bench 标志来测试提供匹配所有内容的正则表达式。您必须将有效的正则表达式传递给 -bench,只是传递 -bench 是语法错误。您可以使用此属性来运行基准测试的子集。

结果的第一行 PASS 来自测试驱动程序的测试部分,要求 go test 运行您的基准测试不会禁用包中的测试。如果你想跳过测试,你可以通过将一个正则表达式传递给 -run 标志来做到这一点,它不会匹配任何东西。我通常使用

go test -run=XXX -bench=.


第二行是被测函数在 b.N 次迭代的最终值下的平均运行时间。在这种情况下,我的笔记本电脑可以在 509 纳秒内执行 Fib(10)。如果有其他与 -bench 过滤器匹配的基准函数,它们将在此处列出。

对各种输入进行基准测试


由于最初的 Fib 函数是经典的递归实现,我们希望它随着输入的增长呈现指数行为。我们可以通过使用 Go 标准库中非常常见的模式稍微重写我们的基准来探索这一点。

func benchmarkFib(i int, b *testing.B) {
        for n := 0; n < b.N; n++ {
                Fib(i)
        }
}

func BenchmarkFib1(b *testing.B)  { benchmarkFib(1, b) }
func BenchmarkFib2(b *testing.B)  { benchmarkFib(2, b) }
func BenchmarkFib3(b *testing.B)  { benchmarkFib(3, b) }
func BenchmarkFib10(b *testing.B) { benchmarkFib(10, b) }
func BenchmarkFib20(b *testing.B) { benchmarkFib(20, b) }
func BenchmarkFib40(b *testing.B) { benchmarkFib(40, b) }

将 benchmarkFib 设为私有可避免测试驱动程序尝试直接调用它,这将失败,因为它的签名与 func(*testing.B) 不匹配。运行这套新的基准测试在我的机器上给出了这些结果。

BenchmarkFib1   1000000000               2.84 ns/op
BenchmarkFib2   500000000                7.92 ns/op
BenchmarkFib3   100000000               13.0 ns/op
BenchmarkFib10   5000000               447 ns/op
BenchmarkFib20     50000             55668 ns/op
BenchmarkFib40         2         942888676 ns/op


除了确认我们简单的 Fib 函数的指数行为之外,在这个基准测试中还有一些其他的事情需要观察。

  • 默认情况下,每个基准测试至少运行 1 秒。如果在 Benchmark 函数返回时秒还没有过去,则 b.N 的值按 1、2、5、10、20、50 ……的顺序增加,然后函数再次运行。
  • 最终的 BenchmarkFib40 只运行了两次,平均每次运行不到一秒。由于测试包使用简单的平均值(在 b.N 上运行基准函数的总时间),因此该结果在统计上很弱。您可以使用 -benchtime 标志增加最短​​基准测试时间以产生更准确的结果。
% go test -bench=Fib40 -benchtime=20s
PASS
BenchmarkFib40        50         944501481 ns/op


年轻玩家的陷阱


上面我提到了 for 循环对于基准驱动程序的操作至关重要。以下是错误的 Fib 基准测试的两个示例。

func BenchmarkFibWrong(b *testing.B) {
        for n := 0; n < b.N; n++ {
                Fib(n)
        }
}

func BenchmarkFibWrong2(b *testing.B) {
        Fib(b.N)
}

在我的系统上,BenchmarkFibWrong 永远不会完成。这是因为基准测试的运行时间会随着 b.N 的增长而增加,而不会收敛到一个稳定的值。 BenchmarkFibWrong2 也同样受到影响并且永远不会完成。

关于编译器优化的说明


在结束之前,我想强调的是,为了完全准确,任何基准测试都应小心避免编译器优化消除被测函数并人为降低基准测试的运行时间。

var result int

func BenchmarkFibComplete(b *testing.B) {
        var r int
        for n := 0; n < b.N; n++ {
                // always record the result of Fib to prevent
                // the compiler eliminating the function call.
                r = Fib(10)
        }
        // always store the result to a package level variable
        // so the compiler cannot eliminate the Benchmark itself.
        result = r
}

结论


Go 中的基准测试工具运行良好,被广泛接受为衡量 Go 代码性能的可靠标准。以这种方式编写基准测试是一种以可重现的方式传达性能改进或回归的绝佳方式。

文章链接