在了解了goroutine
的运作方式之后,接下来我们要考虑的就是如何通过goroutine
来提高性能。本节在进行性能测试时将沿用上一节定义的print1
、goPrint1
等函数,但为了避免这些函数在并发执行时输出一些乱糟糟的结果,这次我们将把代码中的fmt.Println
语句注释掉。代码清单9-4展示了为print1
函数和goPrint1
函数设置的基准测试用例,这些用例定义在goroutine_test.go
文件中。
代码清单9-4 为无goroutine
和有goroutine
的函数分别创建基准测试用例
func BenchmarkPrint1(b *testing.B) {//对顺序执行的函数进行基准测试
for i := 0; i < b.N; i++ {
print1()
}
}
func BenchmarkGoPrint1(b *testing.B) {//对以goroutine形式执行的函数进行基准测试
for i := 0; i < b.N; i++ {
goPrint1()
}
}
在使用以下命令进行性能基准测试并跳过功能测试之后:
go test -run x -bench . –cpu 1
我们将看到以下结果:
BenchmarkPrint1 100000000 13.9 ns/op
BenchmarkGoPrint1 1000000 1090 ns/op
(运行这个测试只使用了单个CPU,具体原因本章稍后将会说到。)正如结果所示,函数print1
运行得非常快,只使用了13.9 ns。令人感到惊讶的是,在使用goroutine
运行相同函数时,程序的速度居然慢了如此之多,足足耗费了1090 ns!出现这种情况的原因在于“天下没有免费的午餐”:无论goroutine
有多么的轻量级,启动goroutine
还是有一定的代价的。因为printNumbers1
函数和printLetters1
函数是如此简单,它们执行的速度是如此快,所以以goroutine
方式执行它们反而会比顺序执行的代价更大。
如果我们对每次迭代都带有一定延迟的printNumbers2
函数和printLetters2
函数执行类似的测试,结果又会如何呢?代码清单9-5展示了goroutine_test.go
文件中为以上两个函数设置的基准测试用例。
代码清单9-5 为无goroutine和有goroutine的带延迟函数分别创建基准测试用例
func BenchmarkPrint2(b *testing.B) {// 对顺序执行的函数进行基准测试
for i := 0; i < b.N; i++ {
print2()
}
}
func BenchmarkGoPrint2(b *testing.B) {// 对以goroutine形式执行的函数进行基准测试
for i := 0; i < b.N; i++ {
goPrint2()
}
}
在运行这一基准测试之后,我们将得到以下结果:
BenchmarkPrint2 10000 121384 ns/op
BenchmarkGoPrint2 1000000 17206 ns/op
这次的测试结果跟上一次的测试结果有些不同。可以看到,以goroutine
方式执行printNumbers2
和printLetters2
的速度是以顺序方式执行这两个函数的速度的差不多7倍。现在,让我们把函数的迭代次数从10次改为100次,然后再运行相同的基准测试:
func printNumbers2() {
for i := 0; i < 100; i++ {
time.Sleep(1 * time.Microsecond)
// fmt.Printf("%d ", i)
}
}
func printLetters2() {
for i := 'A'; i < 'A'+100; i++ {
time.Sleep(1 * time.Microsecond)
// fmt.Printf("%c ", i)
}
}
下面是这次基准测试的结果:
BenchmarkPrint1 20000000 86.7 ns/op
BenchmarkGoPrint1 1000000 1177 ns/op
BenchmarkPrint2 2000 1184572 ns/op
BenchmarkGoPrint2 1000000 17564 ns/op
在这次基准测试中,print1
函数的基准测试时间是之前的13倍,而goPrint1
函数的速度跟上一次相比没有出现太大变化。另一方面,通过延迟模拟负载的函数的测试结果变化非常之大——以顺序方式执行的函数和以goroutine
方式执行的函数之间,两者的执行时间相差了67倍之多。因为这次基准测试的迭代次数比之前增加了10倍,所以print2
函数在进行基准测试时的速度差不多是上次的1/10,但对于goPrint2
来说,迭代10次所需的时间跟迭代100次所需的时间却几乎是相同的。
注意,到目前为止,我们都是在用一个CPU执行测试,但如果我们执行以下命令,改用两个CPU执行带有100次迭代的基准测试:
go test -run x -bench . -cpu 2
那么我们将得到以下结果:
BenchmarkPrint1-2 20000000 87.3 ns/op
BenchmarkGoPrint2-2 5000000 391 ns/op
BenchmarkPrint2-2 1000 1217151 ns/op
BenchmarkGoPrint2-2 200000 8607 ns/op
因为print1
函数以顺序方式执行,无论运行时环境提供多少个CPU,它都只能使用一个CPU,所以它这次的测试结果跟上一次的测试结果基本相同。与此相反,goPrint1
函数这次因为使用了两个CPU来分担计算负载,所以它的性能提高了将近3倍。此外,因为print2
也只能使用一个CPU,所以它这次的测试结果也跟预料中的一样,并没有发生什么变化。最后,因为goPrint2
使用了两个CPU来分担计算负载,所以它这次的测试比之前快了两倍。
现在,如果我们更进一步,使用4个CPU来运行相同的基准测试,结果将会如何?
BenchmarkPrint1-4 20000000 90.6 ns/op
BenchmarkGoPrint1-4 3000000 479 ns/op
BenchmarkPrint2-4 1000 1272672 ns/op
BenchmarkGoPrint2-4 300000 6193 ns/op
正如我们预期的那样,print1
函数和print2
函数的测试结果还是一如既往地没有发生什么变化。但令人惊奇的是,尽管goPrint1
在使用4个CPU时的测试结果还是比只使用一个CPU时的测试结果要好,但使用4个CPU的执行速度居然比使用两个CPU的执行速度要慢。与此同时,虽然只有40%的提升,但goPrint2
在使用4个CPU时的成绩还是比使用2个CPU时的成绩要好。使用更多CPU并没有带来性能提升反而导致性能下降的原因跟之前提到的一样:在多个CPU上调度和运行任务需要耗费一定的资源,如果使用多个CPU带来的性能优势不足以抵消随之而来的额外消耗,那么程序的性能就会不升反降。
从上述测试我们可以看出,增加CPU的数量并不一定会带来性能提升,更重要的是要理解代码,并对其进行基准测试,以了解它的性能特质。