8.4.3 内联操作
我们先看一下内联操作的总体逻辑:
func InlinePackage() {
ir.VisitFuncsBottomUp(typecheck.Target.Decls, func(scc []*ir.Func, recursive bool) {
numfns := numNonClosures(scc)
for _, n := range scc {
// 判断递归调用场景
if !recursive || numfns > 1 {
CanInline(n)
} else {
if base.Flag.LowerM > 1 {
fmt.Printf("%v: cannot inline %v: recursive\n", ir.Line(n), n.Nname)
}
}
InlineCalls(n)
}
})
}
对于递归调用,只有函数调用自己这种场景是不允许内联的,如果多个函数形成一个调用环,那么编译器依然会尝试着对其进行内联。
函数
InlineCalls()
完成实际的内联操作,给定函数fn
, 内联操作的步骤如下: - 1.遍历函数体的各个语句,尝试对其进行内联。尝试内联的操作由函数
inlnode()
完成 - 2.如果当前节点可以内联(肯定是一个函数调用),则创建一个内联节点替换当前节点,内联节点是类型
ir.InlinedCallExpr
, 该操作由mkinlcall
完成 - 3.对于新创建的内联节点,对其所有语句递归地进行内联操作
func B() {
println("B")
}
func A() {
println("A")
B()
}
func C() {
println("C")
D()
}
func D() {
println("D")
C()
}
func main() {
A()
C()
}
以及函数调用的SCC:

Func Call Stack & SCC
我们以
scc = [C, D]
为例来看一下编译器做内联的详细步骤: - 1.遍历 scc, 处理节点 C, 函数
CanInline(C)
识别出该函数可以内联,并为其创建内联属性 Inl - 2.调用
InlineCalls(C)
对 C 进行内联操作 遍历 C 的函数体,并尝试着对每个语句进行内联:第一个语句println("C")
不用内联,第二个语句D()
有可能可以内联,但此时函数 D 还没有经过CanInline()
的内联检查,因为内联属性 Inl
为空,不满足内联条件。到此对 C 的内联操作结束,并没有任何实际的内联操作发生 - 3.遍历 scc, 处理节点 D, 函数
CanInline(D)
识别出该函数可以内联,并为其创建内联属性 Inl. - 4.调用
InlineCalls(D)
对 D 进行内联操作 逻辑同第2步,不同的是编译器发现 语句C()
可以进行内联,于是完成内联后函数 D 变为:
func D() {
println("D")
// Inlined statements
println("C")
D()
}
5. 完成对 scc 的遍历,结束后两个函数的结果为:
func C() {
println("C")
D()
}
func D() {
println("D")
// Inlined statements
println("C")
D()
}
由此可见随着自底向上处理完函数的所有 scc, 整个函数内联操作随即完成。