初探Golang内联

描述

开篇

今天我们来聊聊 Golang 中的内联。

我们知道,函数调用本身是存在成本的。如果把一个实际调用的函数产生的指令,直接插入到的位置,来替换对应的函数调用指令。就可以消除掉这部分性能损耗。但同时也要注意,我们需要维护各个模块的可读性,需要保证高内聚,低耦合,不可能把所有逻辑合到一个函数,这样可读性大大降低。

那么,既然在代码层面做不太好,还有没有别的招呢?

内联就是来做这件事的。下面我们一起来看一下。

内联

所谓内联,指的是编译期间,直接将调用函数的地方替换为函数的实现,它可以减少函数调用的开销以提高程序的性能。内联函数是直接复制“镶嵌”到主函数中去的,就是将内联函数的代码直接放在内联函数的位置上,

这与一般函数不同,主函数在调用一般函数的时候,是指令跳转到被调用函数的入口地址,执行完被调用函数后,指令再跳转回主函数上继续执行后面的代码;而由于内联函数是将函数的代码直接放在了函数的位置上,所以没有指令跳转,指令按顺序执行。Go程序编译时,默认将进行内联优化。

当然,内联也并不是没有代价,这本质是一种以空间换时间的优化方法,其带来的优点是使CPU需要执行的指令数变少了,不需要根据地址跳转的过程了,不用压栈和出栈的过程了,我们把可以复用的程序指令在调用它的地方完全展开了。如果一个函数在很多地方都被调用了,那么就会展开很多次,整个程序占用的空间就会变大了。

需要注意,内联也是有门槛的,并不是随便一个函数调用都可以原地替换。Golang 编译器内部会有一套自己的判断规则,判断一次函数调用能否被内联,后面的章节我们会提到。这也是为什么我们会说:

Inlining is the act of combining smaller functions into their respective callers.

这个 small 的程度很关键。

简单小结一下,内联带来的好处有两个:

解除函数调用的开销,以空间换时间;

支持编译器更有效地应用其他优化策略。

函数调用开销

一个goroutine会有一个单独的栈,栈又会包含多个栈帧,栈帧是函数调用时在栈上为函数所分配的区域。函数调用存在一些固定开销:

创建栈帧;

读写寄存器;

栈溢出检测。

内联什么时候最有效

函数执行的开销 vs 函数调用的开销。这两个开销的比值会很大程度上决定【内联】的效果。

内联其实就是把函数调用这份固定开销给消除了,所以尤其对于函数体极其简单的函数有效果。如果你的函数执行了一系列复杂逻辑,开销远超【函数调用】本身,这里的优化就微不足道了。

内联虽然可以减少函数调用的开销,但是也可能因为存在重复代码,从而导致 CPU 缓存命中率降低,所以并不能盲目追求过度的内联,需要结合 profile 结果来具体分析。

Golang 编译器对内联的要求

参考官方 wiki:github.com/golang/go/w…[1]

编译器

想要内联,方法本身必须满足以下条件:

函数足够简单,当解析AST时,Go申请了80个节点作为内联的预算。每个节点都会消耗一个预算。函数的开销不能超过这个预算;

不能包含闭包,defer,recover,select;

不能以 go:noinline 或 go:unitptrescapes 开头;

必须有函数体;

其他等复杂要求,详细可见src/cmd/compile/internal/gc/inl.go相关内容。我们可以使用 gcflags 参数来判断能不能内联。

内联的实现原理建议大家看看这篇文章:gocompiler.shizhz.me/8.-golang-b…[2]

如何禁止内联

单个函数级别:在函数定义前一行添加//go:noinline;

全局编译级别:可通过-gcflags="-l"选项全局禁用内联,与一个-l禁用内联相反,如果传递两个或两个以上的-l则会打开内联,并启用更激进的内联策略。

gcflags

go build 时可以使用 -gcflags 指定编译选项,-gcflags 参数的格式是:

 

-gcflags="pattern=arg list"

 

pattern 是选择包的模式,arg list 是空格分割的编译选项,如果编译选项中含有空格,可以使用引号包起来。

如:-gcflags="all=-N -l" 代表的是表示主模块和它所有的依赖都禁用【编译器优化】和【内联】。更多编译选项参照 go tool compile --help

Use -gcflags -m to observe the result of escape analysis and inlining decisions for the gc toolchain.

使用 go build 编译时,我们可以使用参数-gflags="-m"运行,可显示被内联的函数,使用运行参数-gflags="-m -m"可以看到原因。类似:

 

./main.go:14:6: cannot inline xxx: unhandled op XXX

/ins.go:9:6: cannot inline xxx: function too complex: cost 104 exceeds budget 80 

 

我们可以用下面的命令分析变量是否逃逸:

 

go run -gcflags '-m -l' main.go

 

-m 其实是打印优化策略的语义,实际上最多总共可以用 4 个 -m,但是信息量较大,一般用 1 个就可以了;

-l 会禁用函数内联,在这里禁用掉内联能更好的观察逃逸情况,减少干扰

内联后堆栈信息还对不对

内联会将函数调用的过程抹掉,这会引入一个新的问题:代码的堆栈信息还能否保证。其实这一点不用担心,Golang 内部会为每个存在内联优化的 goroutine 维持一个内联树(inlining tree),该树可通过 -gcflags="-d pctab=pctoinline" 命令查看,Go在生成的代码中映射了内联函数。并且,也映射了行号。这张表被嵌入到了二进制文件中,所以在运行时可以得到准确的堆栈信息。

  审核编辑:汤梓红

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分