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

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

英文原文链接   发表于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编译器同样可以将这些函数内联进来。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,896评论 18 399
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 135,301评论 19 139
  • 前言 人生苦多,快来 Kotlin ,快速学习Kotlin! 什么是Kotlin? Kotlin 是种静态类型编程...
    任半生嚣狂阅读 26,344评论 9 118
  • 翻译原文链接 转帖/转载请注明出处 英文原文链接 发表于2014/06/07 Goroutine的栈管理 在上一篇...
    曼托斯阅读 4,097评论 0 5
  • 原文地址:C语言函数调用栈(一)C语言函数调用栈(二) 0 引言 程序的执行过程可看作连续的函数调用。当一个函数执...
    小猪啊呜阅读 10,170评论 1 19