乐趣区

关于后端:Go编译原理系列9函数内联

前言

在前一篇文章中分享了编译器优化的变量捕捉局部,本文分享编译器优化的另一个内容—函数内联 函数内联是指将将较小的函数内容,间接放入到调用者函数中,从而缩小函数调用的开销

函数内联概述

咱们晓得每一个高级编程语言的函数调用,老本都是在与须要为它调配栈内存来存储参数、返回值、局部变量等等,Go 的函数调用的老本在于 参数与返回值栈复制 较小的栈寄存器开销 以及 函数序言局部的查看栈扩容 Go 语言中的栈是能够动静扩容的 因为 Go 在调配栈内存不是逐步减少的,而是一次性调配,这样是为了防止拜访越界,它会一次性调配,当查看到调配的栈内存不够用时,它会扩容一个足够大的栈空间,并将原来栈中的内容拷贝过去

下边写一段代码,通过 Go 的基准测试来测一下函数内联带来的效率晋升

import "testing"

//go:noinline // 禁用内联。如果要开启内联,将该行正文去掉即可
func max(a, b int) int {
    if a > b {return a}

    return b
}

var Result int
func BenchmarkMax(b *testing.B)  {
    var r int
    for i:=0; i< b.N; i++ {r = max(-1, i)
    }
    Result = r
}

在编译的过程中,Go 的编译器其实会计算函数内联破费的老本,所以只有简略的函数,才会触发函数内联。在后边函数内联的源码实现中,咱们能够看到下边这些状况 不会被内联

  • 递归函数
  • 函数前有如下正文的:go:noinlinego:noracego:nocheckptrgo:uintptrescapes
  • 没有函数体
  • 函数申明的形象语法树中节点数大于 5000(我的 Go 版本是 1.16.6)(也就是函数外部语句太多的状况,也不会被内联)
  • 函数中蕴含闭包(OCLOSURE)、range(ORANGE)、select(OSELECT)、go(OGO)、defer(ODEFER)、type(ODCLTYPE)、返回值是函数(ORETJMP)的,都不会内联

咱们也能够构建或编译的时候,通过参数去管制它是否能够内联。如果心愿程序中所有的函数都不执行内联操作

go build -gcflags="-l" xxx.go
go tool compile -l xxx.go

同样咱们在编译时,也能够查看哪些函数内联了,哪些函数没内联,以及起因是什么

go tool compile -m=2 xxx.go

看一个例子

package main

func test1(a, b int) int {return a+b}

func step(n int) int {
    if n < 2 {return n}
    return step(n-1) + step(n-2)
}

func main()  {test1(1, 2)
    step(5)
}

能够看到 test1 这个函数是能够内联的,因为它的函数体很简略。step 这个函数因为是递归函数,所以它不会进行内联

函数内联底层实现

这里边其实每一个函数调用链都很深,我这里不会一行一行的解释代码的含意,仅仅会将一些外围的办法拿进去介绍一下,感兴趣的小伙伴能够本人去调试一下(前边有发相干文章)(Go 源码调试办法)

还是前边提到屡次的 Go 编译入口文件,你能够在入口文件中找到这段代码

Go 编译入口文件:src/cmd/compile/main.go -> gc.Main(archInit)

// Phase 5: Inlining
if Debug.l != 0 {
        // 查找能够内联的函数
        visitBottomUp(xtop, func(list []*Node, recursive bool) {numfns := numNonClosures(list)
            for _, n := range list {
                if !recursive || numfns > 1 {caninl(n)
                } else {......}
                inlcalls(n)
            }
        })
    }

    for _, n := range xtop {
        if n.Op == ODCLFUNC {devirtualize(n)
        }
    }

下边就看一下每个办法都在做哪些事件

visitBottomUp

该办法有两个参数:

  • xtop:前边曾经见过它了,它寄存的是每个申明语句的形象语法树的根节点数组
  • 第二个参数是一个函数(该函数也有两个参数,一个是满足是函数类型申明的形象语法树根节点数组,一个是 bool 值,true 示意是递归函数,false 示意不是递归函数)

进入到 visitBottomUp 办法中,你会发现它次要是遍历 xtop,并对每个形象语法树的根节点调用了 visit 这个办法(仅针对是函数类型申明的形象语法树)

func visitBottomUp(list []*Node, analyze func(list []*Node, recursive bool)) {
    var v bottomUpVisitor
    v.analyze = analyze
    v.nodeID = make(map[*Node]uint32)
    for _, n := range list {if n.Op == ODCLFUNC && !n.Func.IsHiddenClosure() { // 是函数,并且不是闭包函数
            v.visit(n)
        }
    }
}

visit 办法的外围是调用了 inspectList 办法,通过 inspectList 对形象语法树依照深度优先搜寻进行遍历,并将每一个节点作为 inspectList 办法的第二个参数(是一个函数)的参数,比方验证这个函数里边是否有递归调用等(具体就是下边的 switch case)

func (v *bottomUpVisitor) visit(n *Node) uint32 {if id := v.nodeID[n]; id > 0 {
        // already visited
        return id
    }

    ......
    v.stack = append(v.stack, n)

    inspectList(n.Nbody, func(n *Node) bool {
        switch n.Op {
        case ONAME:
            if n.Class() == PFUNC {......}
        case ODOTMETH:
            fn := asNode(n.Type.Nname())
            ......
            }
        case OCALLPART:
            fn := asNode(callpartMethod(n).Type.Nname())
            ......
        case OCLOSURE:
            if m := v.visit(n.Func.Closure); m < min {min = m}
        }
        return true
    })
        v.analyze(block, recursive)
    }

    return min
}

后边通过调用 visitBottomUp 的第二个参数传递的办法,对形象语法树进行内联的判断及内联操作,具体就是 caninlinlcalls这两个办法

caninl

该办法的作用就是验证是函数类型申明的形象语法树是否能够内联

这个办法的实现很简略,首先是通过很多的 if 语句验证函数前边是否有像 go:noinline 等这种标记

func caninl(fn *Node) {
    if fn.Op != ODCLFUNC {Fatalf("caninl %v", fn)
    }
    if fn.Func.Nname == nil {Fatalf("caninl no nname %+v", fn)
    }

    var reason string // reason, if any, that the function was not inlined
    ......

    // If marked "go:noinline", don't inline
    if fn.Func.Pragma&Noinline != 0 {
        reason = "marked go:noinline"
        return
    }

    // If marked "go:norace" and -race compilation, don't inline.
    if flag_race && fn.Func.Pragma&Norace != 0 {
        reason = "marked go:norace with -race compilation"
        return
    }

    ......

    // If fn has no body (is defined outside of Go), cannot inline it.
    if fn.Nbody.Len() == 0 {
        reason = "no function body"
        return
    }

    visitor := hairyVisitor{
        budget:        inlineMaxBudget,
        extraCallCost: cc,
        usedLocals:    make(map[*Node]bool),
    }
    if visitor.visitList(fn.Nbody) {
        reason = visitor.reason
        return
    }
    if visitor.budget < 0 {reason = fmt.Sprintf("function too complex: cost %d exceeds budget %d", inlineMaxBudget-visitor.budget, inlineMaxBudget)
        return
    }

    n.Func.Inl = &Inline{
        Cost: inlineMaxBudget - visitor.budget,
        Dcl:  inlcopylist(pruneUnusedAutos(n.Name.Defn.Func.Dcl, &visitor)),
        Body: inlcopylist(fn.Nbody.Slice()),
    }
    ......
}

这里边还有一个次要的办法就是visitList,它是用来验证函数里边是否有咱们上边提到的 go、select、range 等等这些语句。对于满足内联条件的,它会将改写该函数申明抽闲语法树的内联字段(Inl)

inlcalls

该办法中就是具体的内联操作,比方将函数的参数和返回值转换为调用者中的申明语句等。里边的调用和实现都比较复杂,这里不粘代码了,大家可自行去看。函数内联的外围办法都在如下文件中

src/cmd/compile/internal/gc/inl.go
退出移动版