脚本之家,脚本语言编程技术及教程分享平台!
分类导航

Python|VBS|Ruby|Lua|perl|VBA|Golang|PowerShell|Erlang|autoit|Dos|bat|

服务器之家 - 脚本之家 - Golang - Go编译原理之函数内联

Go编译原理之函数内联

2022-08-05 15:46书旅 Golang

这篇文章主要为大家介绍了Go编译原理之函数内联示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

 

前言

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

 

函数内联概述

我们知道每一个高级编程语言的函数调用,成本都是在与需要为它分配栈内存来存储参数、返回值、局部变量等等,Go的函数调用的成本在于参数与返回值栈复制、较小的栈寄存器开销以及函数序言部分的检查栈扩容(Go语言中的栈是可以动态扩容的,因为Go在分配栈内存不是逐渐增加的,而是一次性分配,这样是为了避免访问越界,它会一次性分配,当检查到分配的栈内存不够用时,它会扩容一个足够大的栈空间,并将原来栈中的内容拷贝过来)

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

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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的编译器其实会计算函数内联花费的成本,所以只有简单的函数,才会触发函数内联。在后边函数内联的源码实现中,我们可以看到下边这些情况不会被内联:

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

我们也可以构建或编译的时候,通过参数去控制它是否可以内联。如果希望程序中所有的函数都不执行内联操作

?
1
2
go build -gcflags="-l" xxx.go
go tool compile -l xxx.go

同样我们在编译时,也可以查看哪些函数内联了,哪些函数没内联,以及原因是什么

?
1
go tool compile -m=2 xxx.go

看一个例子

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main
func test1(a, b int) int {
    return a+b
}
func step(n int) int {
    if n &lt; 2 {
        return n
    }
    return step(n-1) + step(n-2)
}
func main()  {
    test1(1, 2)
    step(5)
}

Go编译原理之函数内联

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

 

函数内联底层实现

这里边其实每一个函数调用链都很深,我这里不会一行一行的解释代码的含义,仅仅会将一些核心的方法拿出来介绍一下,感兴趣的小伙伴可以自己去调试一下(前边有发相关文章)(Go源码调试方法

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

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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这个方法(仅针对是函数类型声明的抽象语法树)

?
1
2
3
4
5
6
7
8
9
10
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)

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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等这种标记

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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

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

?
1
src/cmd/compile/internal/gc/inl.go

以上就是Go编译原理之函数内联的详细内容,更多关于Go编译原理函数内联的资料请关注服务器之家其它相关文章!

原文链接:https://juejin.cn/post/7128202181722767396

延伸 · 阅读

精彩推荐
  • Golang在 Golang 中如何快速判断字符串是否在一个数组中

    在 Golang 中如何快速判断字符串是否在一个数组中

    在使用 Python 的时候,如果要判断一个字符串是否在另一个包含字符串的列表中,可以使用in 关键词。...

    未闻Code131262020-11-04
  • GolangGo语言七篇入门教程二程序结构与数据类型

    Go语言七篇入门教程二程序结构与数据类型

    这篇文章主要为大家介绍了Go语言的程序结构与数据类型,本篇文章是Go语言七篇入门系列文,有需要的朋友可以借鉴参考下,希望能够有所帮助...

    小生凡一8642021-12-01
  • Golang浅谈Go Channel 高级实践

    浅谈Go Channel 高级实践

    这篇文章主要介绍了浅谈Go Channel 高级实践,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧 ...

    sxssxs6742020-05-19
  • GolangGo语言中关闭带缓冲区的频道实例分析

    Go语言中关闭带缓冲区的频道实例分析

    这篇文章主要介绍了Go语言中关闭带缓冲区的频道,实例分析了带缓冲区频道的原理与用法,以及关闭带缓冲区频道的技巧,具有一定参考借鉴价值,需要的朋友...

    Go语言编程实例4962020-04-18
  • GolangGolang开发命令行之flag包的使用方法

    Golang开发命令行之flag包的使用方法

    这篇文章主要介绍Golang开发命令行及flag包的使用方法,日常命令行操作,相对应的众多命令行工具是提高生产力的必备工具,本文围绕该内容展开话题,需...

    山山仙人博客6712021-11-23
  • Golanggolang如何去除多余空白字符(含制表符)

    golang如何去除多余空白字符(含制表符)

    这篇文章主要介绍了golang去除多余空白字符(含制表符)的操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...

    跑码场8882021-05-30
  • GolangGo 中如何强制关闭 TCP 连接

    Go 中如何强制关闭 TCP 连接

    本文我们介绍了 TCP 默认关闭与强制关闭两种方式(其实还有种折中的方式:SetLinger(sec > 0)),它们都源于 TCP 的协议设计。...

    Golang技术分享10502021-09-27
  • Golanggo语言map字典删除操作的方法

    go语言map字典删除操作的方法

    这篇文章主要介绍了go语言map字典删除操作的方法,实例分析了map字典操作的技巧,具有一定参考借鉴价值,需要的朋友可以参考下 ...

    秋风秋雨4212020-04-19