1.8 函数内联
函数内联指将较小的函数直接组合进调用者的函数。这是现代编译器优化的一种核心技术。函数内联的优势在于,可以减少函数调用带来的开销。对于Go语言来说,函数调用的成本在于参数与返回值栈复制、较小的栈寄存器开销以及函数序言部分的检查栈扩容(Go语言中的栈是可以动态扩容的),详见第9章。同时,函数内联是其他编译器优化(例如无效代码消除)的基础。我们可以通过一段简单的程序衡量函数内联带来的效率提升[3],如下所示,使用bench对max函数调用进行测试。当我们在函数的注释前方加上//go:noinline时,代表当前函数是禁止进行函数内联优化的。取消该注释后,max函数将会对其进行内联优化。
通过下面的bench对比结果可以看出,在内联后,max函数的执行时间显著少于非内联函数调用花费的时间,这里的消耗主要来自函数调用增加的执行指令。
Go语言编译器会计算函数内联花费的成本,只有执行相对简单的函数时才会内联。函数内联的核心逻辑位于gc/inl.go中。当函数内部有for、range、go、select等语句时,该函数不会被内联,当函数执行过于复杂(例如太多的语句或者函数为递归函数)时,也不会执行内联。
另外,如果函数前的注释中有go:noinline标识,则该函数不会执行内联。如果希望程序中所有的函数都不执行内联操作,那么可以添加编译器选项“-l”。在之后的章节中会频繁地使用这种技巧进行调试。
在调试时,可以获取当前函数是否可以内联,以及不可以内联的原因。
在上面的代码中,当在编译时加入-m=2标志时,可以打印出函数的内联调试信息。可以看出,small函数可以被内联,而fib(斐波那契)函数为递归函数,不能被内联。
当函数可以被内联时,该函数将被纳入调用函数。如下所示,a:=b+f(1),其中,f函数可以被内联。
函数参数与返回值在编译器内联阶段都将转换为声明语句,并通过goto语义跳转到调用者函数语句中,上述代码的转换形式如下,在后续编译器阶段还将对该内联结构做进一步优化。