Go编译原理之函数内联
作者:书旅 发布时间:2024-05-22 10:12:38
前言
在前一篇文章中分享了编译器优化的变量捕获部分,本文分享编译器优化的另一个内容—函数内联。函数内联是指将将较小的函数内容,直接放入到调用者函数中,从而减少函数调用的开销
函数内联概述
我们知道每一个高级编程语言的函数调用,成本都是在与需要为它分配栈内存来存储参数、返回值、局部变量等等,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:noinline
、go:norace
、go:nocheckptr
、go: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
的第二个参数传递的方法,对抽象语法树进行内联的判断及内联操作,具体就是caninl
和inlcalls
这两个方法
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
来源:https://juejin.cn/post/7128202181722767396


猜你喜欢
- 应用场景域名资产监控,通过输入一个主域名,找到该域名对应的ip地址所在的服务器的端口开闭情况。通过定期做这样的监控,有助于让自己知道自己的资
- /// <summary> /// 获得目标
- MySQL5.6主从复制(读写分离)教程1、MySQL5.6开始主从复制有两种方式:基于日志(binlog);基于GTID(全局事务标示符)
- 今天,我们来分享一个宠物桌面小程序,全程都是通过 PyQT 来制作的,对于 Python GUI 感兴趣的朋友,千万不要错过哦!我们先来看看
- 关于scapyScapy 是一个可以让用户发送、侦听和解析并伪装网络报文的Python程序。这些功能可以用于制作侦测、扫描和攻击网络的工具。
- 前言今天我的 PyCharm 突然间就死掉了,双击图标,等半天没有反应,也没有抛出什么错误。打开任务管理器,发现双击时启动了一个PyChar
- 在原txt文件中,我们需要匹配出的字符串为:休闲服务(中间参杂着换行)直接复制到notebook里进行处理①发现需要拿出的字符串都在证卷研究
- python 打开浏览器,可以做简单的刷网页的小程序。仅供学习,别用非法用途。python的webbrowser模块支持对浏览器进行一些操作
- 本文实例讲述了mysql自定义函数原理与用法。分享给大家供大家参考,具体如下:本文内容:什么是函数函数的创建函数的调用函数的查看函数的修改函
- torch.nn.Modules 相当于是对网络某种层的封装,包括网络结构以及网络参数和一些操作torch.nn.Module 是所有神经网
- 先说需求: 1、django 自带了admin后管,如果我们需要使用,只需把我们定义的models注册即可;2、但如果只是简单注册,那显示的
- 所用拓展模块 xlrd: Python语言中,读取Excel的扩展工
- python中的with语句使用于对资源进行访问的场合,保证不管处理过程中是否发生错误或者异常都会执行规定的__exit__(“清理”)操作
- 前言在很多网站中,基本上的都会有一个开头和一个结尾,在每一个网页中都会显示。相对于这种的来说,在Django中,最好的方法就是使用inclu
- map()是一个 Python 内建函数,它允许你不需要使用循环就可以编写简洁的代码。一、Python map() 函数这个map()函数采
- 在项目中遇到后台数据还没有加载完毕,但是页面上调用了后台数据中的字段,这样就会报undefined。例如:一进入页面直接回显数据。我在cre
- js中报404是经常出现的问题,下列是一些高频原因;<script src="${pageContext.request.c
- 文件的一般操作步骤打开文件或创建新文件:使用相应的编程语言和对应的库或模块打开一个已经存在的文件或者创建新文件。读取文件内容或向文件中写入内
- 详解java调用ffmpeg转换视频格式为flv注意:下面的程序是在Linux下运行的,如果在windows下rmvb转换成avi会出现问题
- 一、分析阶段 一般来说,在系统分析阶段往往有太多需要关注的地方,系统各种功能性、可用性、可靠性、安全性需求往往吸引了我们大部分的注意力,但是