当前位置: 首页 > 科技观察

Go中的内联优化

时间:2023-03-12 07:43:15 科技观察

本文讨论了Go编译器如何实现内联,以及这种优化方法如何影响您的Go代码。请注意:本文重点介绍gc,这是来自golang.org的事实上的标准Go编译器。所讨论的概念广泛适用于其他Go编译器,例如gccgo和llgo,尽管它们在实现和效率上可能有所不同。什么是内联?内联是在调用它的地方扩展一个短函数。在计算的早期,这种优化是由程序员手动实现的。内联现在是编译期间自动实现的基本优化过程的一部分。为什么内联很重要?有两个原因。首先是它消除了函数调用本身的开销。第二个是它使编译器能够更有效地执行其他优化策略。函数调用开销在任何语言中,调用函数1都是有成本的。将参数编组到寄存器或将它们放入堆栈(取决于ABI),反之亦然,当返回结果时,会产生开销。引入函数调用会导致程序计数器从指令流中的一个点跳到另一个点,这会导致流水线停顿。函数内部通常有一个pre-processingpreamble,需要为函数执行准备一个新的stackframe,还有一个类似于pre-processing的post-processingepilogue,需要释放stackframe空间,然后返回给调用者.Go中的函数调用会消耗额外的资源来支持堆栈的动态增长。当进入一个函数时,goroutine可用的堆栈空间与函数所需的空间量进行比较。如果可用空间不同,预处理将跳转到运行时逻辑,通过将数据复制到更大的新空间来增加堆栈空间。当这个拷贝完成后,运行时会跳回到原来的函数入口,然后进行栈空间检查。现在检查已经通过,函数调用继续执行。这样goroutine就可以从小的栈空间开始,需要的时候再申请更大的空间。2这个检查消耗很少,只有几条指令,而且由于goroutine栈呈几何级数增长,所以这个检查很少失败。这样,现代处理器的分支预测单元可以通过假设检查肯定会成功来隐藏堆栈空间检查的成本。当处理器错误地预测堆栈空间检查并且不得不放弃它在推测执行中所做的事情时,管道延迟比运行增加goroutine堆栈空间所需的操作所消耗的资源要便宜。虽然现代处理器可以使用预测执行技术来优化每个函数调用中通用和Go-specific元素的开销,但这些开销无法完全消除,因此在对每个函数调用执行必要工作的过程中会有性能提升。消耗。函数调用本身有固定的开销,调用小函数比调用大函数更昂贵,因为它们在每次调用期间做的有用工作较少。因此,消除这种开销的方法必须是消除函数调用本身,这就是Go的编译器所做的,通过在特定条件下将函数调用替换为函数的内容。这个过程称为内联,因为它在调用函数的地方扩展了函数体。ImprovedOptimizationOpportunitiesDr.CliffClick将内联描述为现代编译器所做的优化,像constantpropagation(LCTT译注:此处作者笔误,原文为常数比例,修正为constantpropagation)和deadcodeelimination,都是Compiler的基础优化方法。事实上,内联可以让编译器看得更深,让编译器可以观察被调用的特定函数的上下文,看到可以进一步简化或完全消除的逻辑。由于内联可以递归执行,因此不仅可以在每个单独的函数上下文中,而且可以在整个函数调用链中做出优化决策。内联实践下面是一个演示内联影响的示例:testing.B){varrintfori:=0;我i{r=-1}else{r=i}}Result=r}再次运行benchmark,我们看看手动内联版本和编译器内联版本的性能:%benchstat{old,new}.txtnameoldtime/opnewtime/opdeltaMax-42.21ns±1%0.48ns±3%-78.14%(p=0.000n=18+18)现在编译器可以在BenchmarkMax中看到inlinemax的结果,您可以执行以前不可能进行的优化。例如,编译器注意到i最初为0并且只会递增,因此所有与i的比较都可以假定i不是负数。因此条件表达式-1>i永远不会为真。5在证明-1>i永远不会为真之后,编译器可以将代码简化为:funcBenchmarkMax(b*testing.B){varrintfori:=0;我