这篇文章主要介绍“Go语言中怎么对栈中函数进行内联”,在日常操作中,相信很多人在Go语言中怎么对栈中函数进行内联问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”Go语言中怎么对栈中函数进行内联”的疑惑有所帮助!接下来,请跟着小编一起来学习吧!
把函数内联到它的调用处消除了调用的开销,为编译器进行其他的优化提供了更好的机会,那么问题来了,既然内联这么好,内联得越多开销就越少,为什么不尽可能多地内联呢?
内联可能会以增加程序大小换来更快的执行时间。限制内联的最主要原因是,创建许多函数的内联副本会增加编译时间,并导致生成更大的二进制文件的边际效应。即使把内联带来的进一步的优化机会考虑在内,太激进的内联也可能会增加生成的二进制文件的大小和编译时间。
内联收益最大的是小函数,相对于调用它们的开销来说,这些函数做很少的工作。随着函数大小的增长,函数内部做的工作与函数调用的开销相比省下的时间越来越少。函数越大通常越复杂,因此优化其内联形式相对于原地优化的好处会减少。
在编译过程中,每个函数的内联能力是用内联预算计算的 1。开销的计算过程可以巧妙地内化,像一元和二元等简单操作,在抽象语法数(AST)中通常是每个节点一个单位,更复杂的操作如 make
可能单位更多。考虑下面的例子:
package main func small() string { s := "hello, " + "world!" return s} func large() 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" return s} func main() { small() large()}
使用 -gcflags=-m=2
参数编译这个函数能让我们看到编译器分配给每个函数的开销:
% go build -gcflags=-m=2 inl.go# command-line-arguments./inl.go:3:6: can inline small with cost 7 as: func() string { s := "hello, world!"; return s }./inl.go:8:6: cannot inline large: function too complex: cost 82 exceeds budget 80./inl.go:38:6: can inline main with cost 68 as: func() { small(); large() }./inl.go:39:7: inlining call to small func() string { s := "hello, world!"; return s }
编译器根据函数 func small()
的开销(7)决定可以对它内联,而 func large()
的开销太大,编译器决定不进行内联。func main()
被标记为适合内联的,分配了 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 编译器只对叶子函数进行内联 —— 只有那些不调用其他函数的函数才有资格。在上一段不确定的语句的探讨内容中,一次函数调用就会让这个函数失去内联的资格。
进入栈中进行内联,就像它的名字一样,能内联在函数调用栈中间的函数,不需要先让它下面的所有的函数都被标记为有资格内联的。栈中内联是 David Lazar 在 Go 1.9 中引入的,并在随后的版本中做了改进。这篇文稿深入探究了保留栈追踪行为和被深度内联后的代码路径里的 runtime.Callers
的难点。
在前面的例子中我们看到了栈中函数内联。内联后,func main()
包含了 func small()
的函数体和对 func large()
的一次调用,因此它被判定为非叶子函数。在过去,这会阻止它被继续内联,虽然它的联合开销小于内联预算。
栈中内联的最主要的应用案例就是减少贯穿函数调用栈的开销。考虑下面的例子:
package main import ( "fmt" "strconv") type Rectangle struct {} //go:noinlinefunc (r *Rectangle) Height() int { h, _ := strconv.ParseInt("7", 10, 0) return int(h)} func (r *Rectangle) Width() int { return 6} func (r *Rectangle) Area() int { return r.Height() * r.Width() } func main() { var r Rectangle fmt.Println(r.Area())}
在这个例子中, r.Area()
是个简单的函数,调用了两个函数。r.Width()
可以被内联,r.Height()
这里用 //go:noinline
指令标注了,不能被内联。3
% go build -gcflags='-m=2' square.go # command-line-arguments./square.go:12:6: cannot inline (*Rectangle).Height: marked go:noinline ./square.go:17:6: can inline (*Rectangle).Width with cost 2 as: method(*Rectangle) func() int { return 6 }./square.go:21:6: can inline (*Rectangle).Area with cost 67 as: method(*Rectangle) func() int { return r.Height() * r.Width() } ./square.go:21:61: inlining call to (*Rectangle).Width method(*Rectangle) func() int { return 6 } ./square.go:23:6: cannot inline main: function too complex: cost 150 exceeds budget 80 ./square.go:25:20: inlining call to (*Rectangle).Area method(*Rectangle) func() int { return r.Height() * r.Width() }./square.go:25:20: inlining call to (*Rectangle).Width method(*Rectangle) func() int { return 6 }
由于 r.Area()
中的乘法与调用它的开销相比并不大,因此内联它的表达式是纯收益,即使它的调用的下游 r.Height()
仍是没有内联资格的。
关于栈中内联的效果最令人吃惊的例子是 2019 年 Carlo Alberto Ferraris 通过允许把 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
% go build -gcflags='-m=2 -l=0' sync 2>&1 | grep '(*Mutex).Lock'../go/src/sync/mutex.go:72:6: can inline (*Mutex).Lock with cost 69 as: method(*Mutex) func() { if "sync/atomic".CompareAndSwapInt32(&m.state, 0, mutexLocked) { if race.Enabled { }; return }; m.lockSlow() }
通过把函数分割成一个简单的不能再被分割的外部函数,和(如果没走到外部函数就走到的)一个处理慢速路径的复杂的内部函数,Carlo 组合了栈中函数内联和编译器对基础操作的支持,减少了非竞争锁 14% 的开销。之后他在 sync.RWMutex.Unlock()
重复这个技巧,节省了另外 9% 的开销。
到此,关于“Go语言中怎么对栈中函数进行内联”的学习就结束了,希望能够解决大家的疑惑。理论与实践的搭配能更好的帮助大家学习,快去试试吧!若想继续学习更多相关知识,请继续关注亿速云网站,小编会继续努力为大家带来更多实用的文章!
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。