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

在Go栈上内联函数

时间:2023-03-16 15:07:02 科技观察

在上一篇文章中,我讨论了叶内联如何让Go编译器减少函数调用的开销,并扩展跨函数边界优化的机会。在本文中,我讨论了内联的局限性以及叶内联与堆栈中内联的比较。内联限制。将函数内联到它的调用位置可以消除调用的开销,并为编译器提供更好的机会来执行其他优化。那么问题来了。既然内联这么好,内联越多成本越低,为什么不尽量内联呢?内联可以用增加的程序大小换取更快的执行时间。限制内联的最重要原因是创建许多函数的内联副本会增加编译时间,并且会产生更大的二进制文件的副作用。即使考虑到内联带来的进一步优化机会,过于激进的内联也会增加生成的二进制文件的大小和编译时间。内联的最大收益是相对于调用它们的成本而言做很少工作的小函数。随着函数大小的增长,与函数调用的开销相比,在函数内部完成的工作节省的时间越来越少。较大的函数通常更复杂,因此优化其内联形式相对于就地优化的好处会减少。内联预算在编译时,计算每个函数的内联能力,内联预算为1。开销的计算过程可以被巧妙地内化。一元和二进制等简单操作通常是抽象语法树(AST)中每个节点一个单元,而make等更复杂的操作可能有更多单元。考虑以下示例:packagemainfuncsmall()string{s:="hello,"+"world!"返回s}funclarge()string{s:="a"s+="b"s+="c"s+="d"s+="e"s+="f"s+="g"s+="h"s+="i"s+="j"s+="k"s+="l"s+="m"s+="n"s+="o"s+=“p”s+=“q”s+=“r”s+=“s”s+=“t”s+=“u”s+=“v”s+=“w”s+="x"s+="y"s+="z"returns}funcmain(){small()large()}使用-gcflags=-m=2参数编译这个函数可以让我们看到分配的开销由编译器对每个函数:%gobuild-gcflags=-m=2inl.go#command-line-arguments./inl.go:3:6:caninlinesmallwithcost7as:func()string{s:="你好,世界!";返回}./inl.go:8:6:无法内联large:函数太复杂:成本82超出预算80./inl.go:38:6:可以内联main和成本68as:func(){small();大()}./inl。go:39:7:内联调用smallfunc()string{s:="hello,world!";returns}编译器根据函数funcsmall()的开销决定可以内联(7),而funclarge()的开销太大,编译器决定不内联funcmain()被标记因为适合内联,分配了68的开销;其中small占用7,调用small函数占用57,剩下的(4)是自己的开销。可以使用-gcflag=-l参数控制内联预算的水平。可以使用以下值:-gcflags=-l=0默认内联级别。-gcflags=-l(或-gcflags=-l=1)禁止内联。-gcflags=-l=2和-gcflags=-l=3现已弃用。与-gcflags=-l=0相比没有区别。-gcflags=-l=4减少非叶函数和通过接口调用的函数的开销。2.不确定语句的优化有些函数虽然内联的代价很小,但是仍然不适合内联,因为太复杂了。这就是函数的不确定性,因为一些操作的语义在内联后很难推导,比如recover和break。其他操作,例如select和go,涉及运行时协调,因此内联后引入的额外开销无法抵消内联的好处。不确定语句还包括for和range,它们不一定很昂贵,但到目前为止还没有针对它们进行优化。栈上函数优化过去,Go编译器只内联叶函数——只有那些不调用其他函数的函数才有资格。正如在上一段的不确定语句中所讨论的,单个函数调用使函数无法被内联。入栈内联,顾名思义,可以在函数调用栈的中间内联函数,而无需先将其下方的所有函数都标记为符合内联条件。栈上内联由DavidLazar在Go1.9中引入,并在后续版本中得到改进。这篇文章深入探讨了保留堆栈跟踪行为和运行时的困难。深度内联代码路径中的调用者。在前面的示例中,我们看到了堆栈上的函数内联。内联后,funcmain()包含了funcsmall()的函数体和对funclarge()的调用,因此判断为非叶函数。在过去,这会阻止它进一步内联,尽管它的联合开销低于内联预算。堆栈内联的主要用例是减少遍历函数调用堆栈的开销。考虑以下示例:packagemainimport("fmt""strconv")typeRectanglestruct{}//go:noinlinefunc(r*Rectangle)Height()int{h,_:=strconv.ParseInt("7",10,0)返回int(h)}func(r*Rectangle)Width()int{return6}func(r*Rectangle)Area()int{returnr.Height()*r.Width()}funcmain(){varrRectanglefmt.Println(r.Area())}在此示例中,r.Area()是一个调用两个函数的简单函数。r.Width()可以被内联,而r.Height()在这里被标记为//go:noinline指令,不能被内联。3%gobuild-gcflags='-m=2'square.go#命令行参数./square.go:12:6:cannotinline(*Rectangle).Height:markedgo:noinline./square.go:17:6:可以内联(*Rectangle).Widthwithcost2as:method(*Rectangle)func()int{return6}./square.go:21:6:caninline(*Rectangle).Areawith成本67为:method(*Rectangle)func()int{returnr.Height()*r.Width()}./square.go:21:61:内联调用(*Rectangle).Widthmethod(*Rectangle)func()int{return6}./square.go:23:6:无法内联main:函数太复杂:成本150超出预算80./square.go:25:20:内联调用(*Rectangle)。区域方法(*Rectangle)func()int{returnr.Height()*r.Width()}./square.go:25:20:内联调用(*Rectangle).Widthmethod(*Rectangle)func()int{return6}由于r.Area()中的乘法与调用它的开销相比并不大,因此内部内联它的表达式是纯粹的增益,即使它的下游调用r.Height()仍然是不符合内联资格的快速路径内联内联对堆栈的影响最令人吃惊的例子是CarloAlbertoFerraris2019提高了sync.Mutex的性能.Lock()通过允许其快速路径(非竞争条件)内联到其调用者。在此更改之前,sync.Mutex.Lock()是一个大型函数,具有许多无法理解的条件,使其不符合内联条件。即使锁可用,调用者也要支付调用sync.Mutex.Lock()的成本。Carlo将sync.Mutex.Lock()拆分为两个函数(他称之为概述自己)。外部sync.Mutex.Lock()方法现在调用sync/atomic.CompareAndSwapInt32()并在CAS(比较和交换)成功时立即返回给调用者。如果CAS失败,函数会走上sync.Mutex.lockSlow()的慢路径,需要注册锁,挂起goroutine。4%gobuild-gcflags='-m=2-l=0'同步2>&1|grep'(*Mutex).Lock'../go/src/sync/mutex.go:72:6:可以内联(*Mutex).Lock成本为69as:method(*Mutex)func(){if"sync/atomic".CompareAndSwapInt32(&m.state,0,mutexLocked){ifrace.Enabled{};返回};米。lockSlow()}Carlo通过将函数拆分为一个不能再拆分的简单外部函数,以及(如果未达到外部函数)一个处理慢速路径的复杂内部函数来组合堆栈上的函数内联和编译器支持基本操作将无竞争锁的开销减少了14%。然后他在sync.RWMutex.Unlock()中重复了这个技巧,又节省了9%的开销。