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:

我们以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, 整个函数内联操作随即完成。

最后更新于