Go编译原理之函数内联_Golang

来源:脚本之家  责任编辑:小易  
目录
前言函数内联概述函数内联底层实现visitBottomUpcaninlinlcalls

前言

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

函数内联概述

我们知道每一个高级编程语言的函数调用,成本都是在与需要为它分配栈内存来存储参数、返回值、局部变量等等,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 &lt; 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

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

您可能感兴趣的文章:Go语言编译原理之源码调试Go语言编译原理之变量捕获go json编译原理XJSON实现四则运算

  • 本文相关:
  • 使用golang的channel交叉打印两个数组的操作
  • go语言中的并发goroutine底层原理
  • golang 实现简单随机负载均衡
  • go语言利用ffmpeg转hls实现简单视频直播
  • go语言使用第三方包 json化结构体操作示例
  • go语言基础类型及常量用法示例详解
  • 小学生也能看懂的golang异常处理recover panic
  • go语言使用组合的方式实现多继承的方法
  • go泛型实战教程之如何在结构体中使用泛型
  • 使用go实现一个超级mini的消息队列的示例代码
  • 成员函数一定是内联函数吗?举例说明一下
  • 不显式声明为内联函数,编译器会自动设为内联吗
  • 内联函数里面有循环为什么编译器不视为内联呢?一个for循环也...
  • 内联函数和编译预处理的区别是?
  • C++编译器(Dev-C)是否会自动内联函数 对于什么样的函数即使...
  • C++内联函数在 在编译时是将该函数的目标代码插入每个调用...
  • C++中内联函数何时被编译器禁止
  • C++中的内联函数是在编译时原地展开的还是在运行时原地展开...
  • 内联函数的定义对编译器而言必须是可见的?什么意思?
  • 内联函数在ndk中如何编译
  • c++定义在类里面的函数,如果编译器一定将他内联处理吗?
  • 内联函数调用时不发生转移控制不用保护现场的意思是内联函数...
  • 优先函数是什么?编译原理
  • codeblocks 如何编译16位内联汇编
  • 什么是内联函数
  • 编译原理项目集规范族问题GO(I,X)中的X是安什么顺序进行测试...
  • 编译原理用C++生成四元式的程序,如果把生成四元式当成一个...
  • 编译原理用C++生成四元式的程序,如果把生成四元式当成一个...
  • 谁能详细说明一下编译器自动调用类型构造函数进行类型转换的...
  • 网站首页网页制作脚本下载服务器操作系统网站运营平面设计媒体动画电脑基础硬件教程网络安全vbsdos/bathtahtcpythonperl游戏相关vba远程脚本coldfusionruby专题autoitseraphzonepowershelllinux shellluagolangerlang其它首页golang变量捕获go源码调试方法go语言编译原理之源码调试go语言编译原理之变量捕获go json编译原理xjson实现四则运算使用golang的channel交叉打印两个数组的操作go语言中的并发goroutine底层原理golang 实现简单随机负载均衡go语言利用ffmpeg转hls实现简单视频直播go语言使用第三方包 json化结构体操作示例go语言基础类型及常量用法示例详解小学生也能看懂的golang异常处理recover panicgo语言使用组合的方式实现多继承的方法go泛型实战教程之如何在结构体中使用泛型使用go实现一个超级mini的消息队列的示例代码go语言string,int,int64 ,float之间类goland激活码破解永久版及安装详细教程(亲测可以)go语言中的array、slice、map和set使用详解go语言的gopath与工作目录详解go语言转换所有字符串为大写或者小写的方法go语言interface详解五步让你成为go 语言高手go语言命令行操作命令详细介绍go语言编程中字符串切割方法小结我放弃python转go语言的9大理由(附优秀书籍推荐)使用go在mangodb中进行crud操作浅谈go语言中的结构体struct & 接口interface & 反射go json编码与解码的实现go语言实现简易比特币系统钱包的原理解析golang替换无法显示的特殊字符(\u0000,?\000,?^@)golang?使用sort.slice包实现对象list排序解决goland新建项目文件名为红色的问题go语言defer语句的三种机制整理go语言中的方法、接口和嵌入类型详解go语言实现基于websocket浏览器通知功能
    免责声明 - 关于我们 - 联系我们 - 广告联系 - 友情链接 - 帮助中心 - 频道导航
    Copyright © 2017 www.zgxue.com All Rights Reserved