Links

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. 1.
    遍历函数体的各个语句,尝试对其进行内联。尝试内联的操作由函数inlnode()完成
  2. 2.
    如果当前节点可以内联(肯定是一个函数调用),则创建一个内联节点替换当前节点,内联节点是类型ir.InlinedCallExpr, 该操作由mkinlcall完成
  3. 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. 1.
    遍历 scc, 处理节点 C, 函数CanInline(C)识别出该函数可以内联,并为其创建内联属性 Inl
  2. 2.
    调用InlineCalls(C)对 C 进行内联操作 遍历 C 的函数体,并尝试着对每个语句进行内联:第一个语句println("C")不用内联,第二个语句D()有可能可以内联,但此时函数 D 还没有经过CanInline()的内联检查,因为内联属性 Inl为空,不满足内联条件。到此对 C 的内联操作结束,并没有任何实际的内联操作发生
  3. 3.
    遍历 scc, 处理节点 D, 函数CanInline(D)识别出该函数可以内联,并为其创建内联属性 Inl.
  4. 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, 整个函数内联操作随即完成。