当前位置: 首页 > 后端技术 > PHP

Go内联优化能让一个程序快多少?

时间:2023-03-30 01:01:17 PHP

大家好,我是炸鱼。周末在家学习的时候看到了@DaveCheney的《Inlining optimisations in Go》。还是有很多养料的。这篇文章解释了Go编译器如何实现内联,以及这种优化将如何影响您的Go代码。接下来开始用炸鱼吸取知识。什么是内联?内联是将较小的函数合并到它们各自的调用者中的行为。它在计算历史的不同时期的做法是不同的,如下:早期:这种优化通常是手工完成的。现在:内联是编译过程中自动发生的基本优化类别之一。为什么内联很重要?内联很重要,每种语言都必须有它。具体原因如下:消除了函数调用本身的开销。它允许编译器更有效地应用其他优化策略。从本质上讲,性能更好。函数调用成本的基础知识用任何语言调用函数都是有成本的。将参数编程到寄存器或堆栈(取决于ABI),并在返回时反转这个过程,是开销。调用函数需要将程序计数器从指令流中的一个点跳到另一个点,这会导致流水线停顿。一旦进入一个函数,通常需要一些序言来为函数的执行准备一个新的堆栈帧,以及一个类似的尾声来在返回给调用者之前退出该帧。Go中的开销在Go中,函数调用有额外的成本来支持动态堆栈增长。在入口处,将goroutine可用的堆栈空间量与函数所需的量进行比较。如果可用的堆栈空间不足,则序言会跳转到运行时逻辑,通过将堆栈复制到更大的新位置来增加堆栈。完成此操作后,运行时将跳回到原始函数的开头,再次进行堆栈检查,现在检查通过,然后继续调用。通过这种方式,goroutines可以从一个小的堆栈分配开始,只在需要时增长。这个检查很便宜,只需要几条指令,而且由于goroutine的堆栈呈几何增长,所以检查很少失败。因此,现代处理器中的分支预测单元可以通过假设堆栈检查总是成功来隐藏堆栈检查的成本。在处理器错误预测堆栈检查并且不得不放弃它在推测执行时所做的工作的情况下,与在运行时增长goroutine堆栈所需的工作成本相比,停滞管道的成本相对较小。Go中的优化虽然现代处理器使用推测执行技术很好地优化了每个函数调用的通用和Go特定组件的开销,但这些开销不能完全消除,因此每个函数调用都会带来性能成本,超过执行有用的时间工作。由于函数调用的开销是固定的,较小的函数比较大的函数支付更多,因为它们在每次调用时往往做的有用工作较少。因此,消除这些开销的解决方案必须是消除函数调用本身,Go编译器在某些条件下通过用函数的内容替换对函数的调用来做到这一点。这称为内联,因为它使函数体与其调用者保持一致。改进优化的机会CliffClick博士将内联描述为现代编译器所做的一种优化,因为它是优化的基础,例如常量传播和死代码消除。事实上,内联允许编译器看得更远,允许它观察在调用特定函数的情况下可以进一步简化或完全消除的逻辑。由于可以递归地应用内联,因此优化决策不仅可以在每个单独函数的上下文中进行,还可以应用于调用路径中的函数链。做内联优化禁止内联内联的效果可以通过这个小例子来演示:BenchmarkMax(b*testing.B){varrintfori:=0;我i{r=-1}else{r=i}}Result=r}再次运行基准测试,我们看到手动内联版本的性能与编译器内联版本一样好。结果如下:%benchstat{old,new}.txtnameoldtime/opnewtime/opdeltaMax-42.21ns±1%0.48ns±3%-78.14%(p=0.000n=18+18)现在,编译器max的结果可以内联到BenchmarkMax中,它可以应用以前不可能的优化方法。例如:编译器注意到i被初始化为0并且只会递增,因此与i的任何比较都可以假定i永远不会为负数。因此,条件-1>i永远不会为真。在证明-1>i永远不可能为真之后,编译器可以将代码简化为:funcBenchmarkMax(b*testing.B){varrintfori:=0;我