基准测试示例

在这节,我将向您介绍一个基本的基准测试示例,该示例测量三个产生斐波纳切序列数字算法的性能。好消息是这些算法需要大量的数学计算,以满足基准测试标准。

为了这节目的,我将创建一个新的 main 包,它将存在 benchmarkMe.go 中,并分三部分来介绍。

benchmarkMe.go 的第一部分如下:

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func fibo1(n int) int{
  6. if n == 0 {
  7. return 0
  8. } else if n == 1 {
  9. return 1
  10. } else {
  11. return fibo1(n-1) + fibo1(n-2)
  12. }
  13. }

上面的代码包含了 fibo1() 函数的实现,该函数使用了递归算法来计算斐波纳切序列数字。尽管这个算法运行的很好,但这是一个相对简单、缓慢的方法。

benchmarkMe.go 的第二段代码如下:

  1. func fibo2(n int) int {
  2. if n == 0 || n == 1 {
  3. return n
  4. }
  5. return fibo2(n-1) + fibo2(n-2)
  6. }

从这部分,您看到了 fibo2() 函数的实现,它几乎和我们之前看到的 fibo1()函数相同。然而,有趣的是,一点点代码的改变(单个 if 表达式而不是 if else if 块)是否对函数的性能有任何影响。

benchmarkMe.go 的第三部分包含另一个计算斐波纳切序列数字的函数实现:

  1. func fibo3(n int) int {
  2. fn := make(map[int]int)
  3. for i := 0; i <= n; i++ {
  4. var f int
  5. if i <= 2 {
  6. f = 1
  7. } else {
  8. f = fn[i-1] + fn[i-2]
  9. }
  10. fn[i] = f
  11. }
  12. return fn[n]
  13. }

在这介绍的 fibo3() 函数使用了一个全新的方法,它需要一个 Go map 和一个 for 循环。这个方法是否真的比其他两种实现更快,还有待观察。在 fibo3() 中介绍的算法也将用在第13章(网络编程 - 构建服务器与客户端),在那将更详细的解释它。一会您就会看到,选择一个高效的算法能减少很多麻烦!

benchmarkMe.go 的其余代码如下:

  1. func main() {
  2. fmt.Println(fibo1(40))
  3. fmt.Println(fibo2(40))
  4. fmt.Println(fibo3(40))
  5. }

执行 benchmarkMe.go 将产生如下输出:

  1. $ go run benchmarkMe.go
  2. 102334155
  3. 102334155
  4. 102334155

好消息是这三种实现都返回了相同的数字。现在是时候给 benchmarkMe.go 添加一些基准测试来理解这三个算法中每一个的效率了。

由于 Go 规则要求,这个包含基准测试函数的 benchmarkMe.go 版本将另存为 benchmarkMe_test.go。这个程序分为五个部分来介绍。

benchmarkMe_test.go 的第一段代码如下:

  1. package main
  2. import (
  3. "testing"
  4. )
  5. var result int
  6. func benchmarkfibo1(b *testing.B, n int) {
  7. var r int
  8. for i := 0; i < b.N; i++ {
  9. r = fibo1(n)
  10. }
  11. result = r
  12. }

从上面的代码,您能看到一个用 benchmark 字符串而不是 Benchmark 开头命名的函数实现。因此,这个函数将不能自动运行,因为它用小写 b 而不是大写B 开头。

存放 fibo1(n) 的结果在一个名为 r 的变量中,并在之后使用另一个名为 result 的全局变量的原因是很微妙。此技巧用于阻止编译器执行任何优化,这些优化将排除您要测量的函数,因为它的结果从未被使用过!相同的技巧将用在接下来介绍的 benchmarkfibo2()benchmarkfibo3() 函数中。

benchmarkMe_test.go 的第二部分显示在如下代码中:

  1. func benchmarkfibo2(b * testing.B, n int) {
  2. var r int
  3. for i := 0; i < b.N; i++ {
  4. r = fibo2(n)
  5. }
  6. result = r
  7. }
  8. func benchmarkfibo3(b * testing.B, n int) {
  9. var r int
  10. for i := 0; i < b.N; i++ {
  11. r = fibo3(n)
  12. }
  13. result = r
  14. }

上面的代码定义了另两个基准测试函数,因为它们以小写 b 开头而不是大写 B 所以不能自动运行。

现在,我来告诉您一个大秘密:即使这三个函数被命名为 BenchmarkFibo1()BenchmarkFibo2()BenchmarkFibo3(),它们也不能被 go test 命令自动调用,因为它们的签名不是 func(*testing.B)。所以,用小写 b 给它们命名的原因如此。然而,没有什么可以阻止您之后从其他基准函数调用它们,稍后您就会看到。

benchmarkMe_test.go 的第三部分如下:

  1. func Benchmark30fibo1(b *testing.B) {
  2. benchmarkfibo1(b, 30)
  3. }

正确的基准函数拥有正确的名称和正确的签名,意味着它将由 go tool 执行。

注意,尽管 Benchmark30fibo1() 是有效的基准函数名,但 BenchmarkfiboIII() 不是因为在 Benchmark 字符串后没有大写字符或数字。这是非常重要的,因为一个拥有无效名称的基准函数不能被自动执行。

benchmarkMe_test.go 的第四段包含如下 Go 代码:

  1. func Benchmark30fibo2(b *testing.B) {
  2. benchmarkfibo2(b, 30)
  3. }
  4. func Benchmark30fibo3(b *testing.B) {
  5. benchmarkfibo3(b, 30)
  6. }

Benchmark30fibo2()Benchmark30fibo3() 基准函数都和 Bencharmk30fibo1() 相同。

benchmarkMe_test.go 都最后部分如下:

  1. func Benchmark50fibo1(b *testing.B) {
  2. benchmarkfibo1(b, 50)
  3. }
  4. func Benchmark50fibo2(b *testing.B) {
  5. benchmarkfibo2(b, 50)
  6. }
  7. func Benchmark50fibo3(b *testing.B) {
  8. benchmarkfibo3(b, 50)
  9. }

在这部分,您看到了另外三个基准函数,用于计算斐波纳切序列中的第50个数。

记住每个基准测试默认执行至少 1 秒。如果基准函数在少于 1 秒的时间内返回,则 b.N 的值增加并且该函数会再次运行。b.N 的值第一次是 1,然后变为 2,5,10,20,50 等等。这是因为函数运行的越快,您就需要越多次的运行它来获得准确结果

执行 benchmarkMe_test.go 将产生如下输出:

11.8.1 基准测试示例 - 图1

这里有俩点很重要:第一,-bench 参数的值指定了将要执行的基准函数。这个被使用的点值是一个正则表达式,用于匹配所有有效的基准函数。第二,如果您忽略了 -bench 参数,将没有基准函数被执行!

那么,这个输出告诉了我们什么?首先,在每个基准函数(Benchmark10fibo1-8)结尾处的 -8 表示该函数被执行期间的 goroutines 数,本质上它是 GOMAXPROCS 环境变量的值。您会记得我们在第10章(揪出隐藏的代码)讨论过 GOMAXPROCS 环境变量。同样,您可以看到 GOOSGOARCH 的值,它们显示了您机器的操作系统和架构。

输出的第二列显示了相关函数的执行次数。较快的函数比较慢的函数被执行了多次。例如,Benchmark30fibo3() 函数执行了 500,000 次,而 Benchmark50fibo2() 函数仅执行了一次!输出的第三列显示了每个运行的平均值。

如您所见,fibo1()fibo2() 函数真的比 fibo3() 函数慢。如果您希望在输出中包含内存分配统计,您可以执行如下命令:

11.8.1 基准测试示例 - 图2

上面的输出和没有使用 -benchmem 命令行参数的输出类似,但它多包含了两列。第四列显示了平均分配给每个执行的基准函数的内存数。第五列显示了用于分配第四列的内存值的分配数。所以,Benchmark50fibo3() 在 10 分配中平均分配了 2481 字节。

如您所知,fibo1()fibo2() 函数除了预期的内存外,都不需要特定类型的内存,这与 fibo3() 使用一个 map 变量的情况不同;因此,Benchmark10fibo3-8 的输出的第四和第五列的值都大于0。