剖析使Go语言高效的5个特性(2/5): 函数调用不是免费的

浏览: 67 发布日期: 2017-08-10 分类: golang

翻译原文链接 转帖/转载请注明出处

英文原文链接 发表于2014/06/07

函数调用不是免费的

一个函数调用有三个步骤。创建一个新的堆栈框(stack frame)并把调用者的详细信息记录下来。把任何会被被调用函数用到的寄存器内容保存到堆栈。计算被调用函数的地址,并执行跳转指令到那个新的地址。

因为函数调用是频繁操作,CPU的设计者花费了很多精力来优化这个过程,但他们不可能消除所有的开销。

根据被调用函数的功能,这个调用开销可能是可以忽略不计的,也可能是非常显著的。有一个降低调用开销的优化技术叫内联(inlining)

Go语言编译器通过把被调用函数代码当作调用者代码的一部分来实现内联。内联也是有代价的。它会增加编译出来的二进制可执行文件的大小。只有在调用函数的开销占到被调用函数本身的工作量很大一部分的时候,内联才有意义。所以只有简单的函数才被考虑启用内联。调用函数的开销往往不占复杂函数的大头,所以他们也就不会被内联。

上面这个例子展示了函数Double对util.Max的调用。为了降低调用util.Max的成本,编译器会把util.Max内联到Double函数里,产生如下内容:

内联之后,util.Max将不会被调用,但是Double的行为并没有改变。内联并不是Go语言独有的。几乎所有编译的或者即时编译(JITed)的语言会提供这项优化。那么Go语言里的内联是怎么工作的呢?

Go语言的实现非常简单。当一个包(package)被编译的时候,任何适合内联的小函数都被标记并且按正常情况编译。然后将源代码和编译后的二进制同时保存下来。

上面的图片显示了util.a的内容。源代码被做了稍微的改动以方便编译器的快速处理。当编译器编译Double的时候,它会发现util.Max是可以内联的并且util.Max的源代码也存在。这时编译器会插入原函数的源代码,而不是插入一个util.Max的调用。

保存源代码还使得其它优化成为可能。

比如上面这个例子,虽然Test函数总是返回false,Expensive在执行它之前是无法知道的。但是当Test被内联的时候,我们就得到了如下的代码:

这样编译器就能知道那块代码是不会被执行到的。

这样不仅节省了调用Test函数的开销,它还节省了编译任何不会被执行的代码。Go编译器能够自动在多个文件或者包(package)之间实现函数内联。如果某些代码调用了来自标准库的可内联函数,Go编译器同样可以将这些函数内联进来。

返回顶部