基准测试示例

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

    benchmarkMe.go 的第一部分如下:

    上面的代码包含了 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. fn[i] = f
    10. }
    11. return fn[n]
    12. }

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

    benchmarkMe.go 的其余代码如下:

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

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

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

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

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

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

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

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

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

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

    benchmarkMe_test.go 的第三部分如下:

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

    正确的基准函数拥有正确的名称和正确的签名,意味着它将由 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. }

    benchmarkMe_test.go 都最后部分如下:

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

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

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

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

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

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

    11.8.1 基准测试示例 - 图2

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