翻译原文连接 转帖/转载请注明出处php
原文连接@hashrocket.com 发表于2015/12/28linux
在开发pgx(一个针对Go语言的PostgreSQL driver)的时候,有好几回我都须要在20多个代码分支间跳转。一般我会选用switch语句。还有个更加可读的实现方法是使用函数map。我一开始认为用switch语句进行分支跳转比一个map查找和函数调用更快。数据库驱动(database driver)的性能是一个很重要的考量,因此在作任何改动前,有必要对它们的影响作一下慎重地研究。git
性能测试显示它们有很大的差别。但最终的答案是它们对整个程序来讲多是可有可无的。若是你想了解得出这个结论而作的测试,那么请继续阅读。github
在互联网上没有找到有用的信息。我找到的几个帖子都认为map在有足够多跳转分支时会更快。一个在2012年对switch优化的讨论包括了Ken Thompson的观点。他认为没有太多优化的空间。我决定写一个benchmark来测试它们在Go语言里的性能。golang
取得下面结果的系统配置是:Intel i7-4790K,Ubuntu 14.04,运行的是go1.5.1 linux/amd64。测试的源代码和结果在Github上。数据库
下面是一个对switch的基本测试:微信
func BenchmarkSwitch(b *testing.B) { var n int for i := 0; i < b.N; i++ { switch i % 4 { case 0: n += f0(i) case 1: n += f1(i) case 2: n += f2(i) case 3: n += f3(i) } } // n will never be < 0, but checking n should ensure that the entire benchmark loop can't be optimized away. if n < 0 { b.Fatal("can't happen") }
众所周知,像这样的测试要达到它的目的一般是很困难的。好比,编译优化器会把一段不产生任何效果的代码彻底忽略掉。这里的n就是用来防止这整段代码不被优化掉。接下来的文章里还会提到其它几个须要注意的地方。app
下面是一个函数map的测试代码:函数
func BenchmarkMapFunc4(b *testing.B) { var n int for i := 0; i < b.N; i++ { n += Funcs[i%4](i) } // n will never be < 0, but checking n should ensure that the entire benchmark loop can't be optimized away. if n < 0 { b.Fatal("can't happen") } }
咱们使用Ruby erb模版来生成包含4,8,16,32,64,128,256和512个跳转分支的测试。结果显示map版本在4个分支的状况下比switch版本慢了25%。在8个分支的状况下它们的性能至关。map版本在分支越多的状况下越快,在512个分支的测试里它会比switch版本快50%。工具
以前的测试给出了一些结果,可是它们并不充分。有好几个影响测试的因素都没有考虑进去。首先是函数是否被内联。一个函数能够在switch语句里被内联,可是函数map就不会。咱们有必要测试一下函数内联对性能的影响。
下面这个函数作了一些毫无心义的工做,它能保证整个函数内容不会被优化掉,可是Go语言的编译器会把整个函数内联。
func f0(n int) int { if n%2 == 0 { return n } else { return 0 } }
在写这篇文章的时候,Go编译器还不能内联包含panic的函数。下面这个函数包含了一个不可能被执行到的panic调用,从而防止了函数被内联。
func noInline0(n int) int { if n < 0 { panic("can't happen - but should ensure this function is not inlined") } else if n%2 == 0 { return n } else { return 0 } }
当函数不能被内联时,性能有了很大的变化。map版本的代码比switch版本在4个分支的测试里快了大约30%,在512个分支的测试里快了300%。
上面的测试根据循环的次数来决定跳转分支。
for i := 0; i < b.N; i++ { switch i % 4 { // ... } }
这保证咱们测试的仅仅是分支跳转的性能。在现实世界中,分支跳转的选择一般会致使一个内存的读取。为了模拟这个行为,咱们使用一个简单的查找来决定跳转分支。
var ascInputs []int func TestMain(m *testing.M) { for i := 0; i < 4096; i++ { ascInputs = append(ascInputs, i) } os.Exit(m.Run()) } func BenchmarkSwitch(b *testing.B) { // ... for i := 0; i < b.N; i++ { switch ascInputs[i%len(ascInputs)] % 4 { // ... } // ... }
这个改变大大的下降了性能。表现最好的switch测试性能从1.99 ns/op降低到了8.18 ns/op。表现最好的map测试性能从2.39 ns/op降低到了10.6 ns/op。具体的数据在不一样的测试中会有一些差异,可是查找操做增长了大约7 ns/op。
厉害的读者确定已经注意到了,这些测试里的分支跳转是高度可预测的,这不符合现实。它老是按照分支0,而后分支1,而后分支2的顺序来。为了解决这个问题,分支跳转的选择被随机化了。
var randInputs []int func TestMain(m *testing.M) { for i := 0; i < 4096; i++ { randInputs = append(randInputs, rand.Int()) } os.Exit(m.Run()) } func BenchmarkSwitch(b *testing.B) { // ... for i := 0; i < b.N; i++ { switch randInputs[i%len(ascInputs)] % 4 { // ... } // ... }
这个改变继续下降了性能。在32个跳转分支的测试里,map查找延迟从11ns涨到了22ns。具体的数据根据跳转分支的数目以及函数是否被内联会有变化,可是性能基本都降低了一半。
从计算跳转分支目的地到查找跳转目的地的性能损失是在预料之中的,由于有了额外的内存读取。可是从顺序跳转到随机跳转的性能影响却出乎意料。为了了解其中的缘由,咱们使用Linux perf工具。它能够提供例如cache miss和分支跳转预测错误(branch-prediction misses)等CPU层面的统计。
为了不对测试程序编译过程的profiling,能够将测试代码预先编译好。
go test -c
而后咱们让perf工具为咱们提供其中一个顺序查找测试的统计数据。
$ perf stat -e cache-references,cache-misses,branches,branch-misses ./go_map_vs_switch.test -test.bench=PredictableLookupMapNoInlineFunc512 -test.benchtime=5s
输出结果里有意思的部分是分支跳转预测错误的统计:
9,919,244,177 branches 10,675,162 branch-misses # 0.11% of all branches
因此当跳转顺序可预测时分支跳转预测工做得很是好。但不可预测分支跳转测试里的结果就大相径庭。
$ perf stat -e cache-references,cache-misses,branches,branch-misses ./go_map_vs_switch.test -test.bench=UnpredictableLookupMapNoInlineFunc512 -test.benchtime=5s
相关的输出:
3,618,549,427 branches 451,154,480 branch-misses # 12.47% of all branches
分支跳转预测的错误率涨了100倍。
我最初想回答的问题是,用函数map来替换switch语句对性能是否会有影响。我假设switch语句会快一点。可是我错了。Map一般会更快,并且快好几倍。
这是否意味着咱们应该选择使用map而不是switch语句呢?不!虽然从百分比来看改变很是大,但绝对的时间差别其实很小。只有在每秒钟执行上百万次分支跳转而没有其它实际工做量时,这个差别才会显现出来。即便是这样,内存访问和分支跳转的成功率对性能影响更大,而不是选择switch语句或者map。
对switch语句或者map的选择标准应该是谁能产生最干净的代码,而不是性能。
若是你喜欢咱们的文章,请关于微信公众号【曼托斯】