Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
191 lines (141 sloc) 7.73 KB

基准测试示例

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

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

benchmarkMe.go 的第一部分如下:

package main
import (
    "fmt"
)
func fibo1(n int) int{
    if n == 0 {
        return 0
    } else if n == 1 {
        return 1
    } else {
        return fibo1(n-1) + fibo1(n-2)
    }
}

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

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

func fibo2(n int) int {
    if n == 0 || n == 1 {
        return n
    }
    return fibo2(n-1) + fibo2(n-2)
}

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

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

func fibo3(n int) int {
    fn := make(map[int]int)
    for i := 0; i <= n; i++ {
        var f int
        if i <= 2 {
            f = 1
        } else {
            f = fn[i-1] + fn[i-2]
        }
        fn[i] = f
    }
    return fn[n]
}

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

benchmarkMe.go 的其余代码如下:

func main() {
    fmt.Println(fibo1(40))
    fmt.Println(fibo2(40))
    fmt.Println(fibo3(40))
}

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

$ go run benchmarkMe.go
102334155
102334155
102334155

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

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

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

package main
import (
    "testing"
)

var result int
func benchmarkfibo1(b *testing.B, n int) {
    var r int
    for i := 0; i < b.N; i++ {
        r = fibo1(n)
    }
    result = r
}

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

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

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

func benchmarkfibo2(b * testing.B, n int) {
    var r int
    for i := 0; i < b.N; i++ {
        r = fibo2(n)
    }
    result = r
}

func benchmarkfibo3(b * testing.B, n int) {
    var r int
    for i := 0; i < b.N; i++ {
        r = fibo3(n)
    }
    result = r
}

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

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

benchmarkMe_test.go 的第三部分如下:

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

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

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

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

func Benchmark30fibo2(b *testing.B) {
    benchmarkfibo2(b, 30)
}

func Benchmark30fibo3(b *testing.B) {
    benchmarkfibo3(b, 30)
}

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

benchmarkMe_test.go 都最后部分如下:

func Benchmark50fibo1(b *testing.B) {
    benchmarkfibo1(b, 50)
}

func Benchmark50fibo2(b *testing.B) {
    benchmarkfibo2(b, 50)
}

func Benchmark50fibo3(b *testing.B) {
    benchmarkfibo3(b, 50)
}

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

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

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

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

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

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

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

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

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

You can’t perform that action at this time.