基准测试示例
在这节,我将向您介绍一个基本的基准测试示例,该示例测量三个产生斐波纳切序列数字算法的性能。好消息是这些算法需要大量的数学计算,以满足基准测试标准。
为了这节目的,我将创建一个新的 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
环境变量。同样,您可以看到 GOOS
和 GOARCH
的值,它们显示了您机器的操作系统和架构。
输出的第二列显示了相关函数的执行次数。较快的函数比较慢的函数被执行了多次。例如,Benchmark30fibo3()
函数执行了 500,000 次,而 Benchmark50fibo2()
函数仅执行了一次!输出的第三列显示了每个运行的平均值。
如您所见,fibo1()
和 fibo2()
函数真的比 fibo3()
函数慢。如果您希望在输出中包含内存分配统计,您可以执行如下命令:
上面的输出和没有使用 -benchmem
命令行参数的输出类似,但它多包含了两列。第四列显示了平均分配给每个执行的基准函数的内存数。第五列显示了用于分配第四列的内存值的分配数。所以,Benchmark50fibo3()
在 10 分配中平均分配了 2481 字节。
如您所知,fibo1()
和 fibo2()
函数除了预期的内存外,都不需要特定类型的内存,这与 fibo3()
使用一个 map 变量的情况不同;因此,Benchmark10fibo3-8
的输出的第四和第五列的值都大于0。