开篇
今天我们来聊聊 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:cannotinlinexxx:unhandledopXXX /ins.go:9:6:cannotinlinexxx:functiontoocomplex:cost104exceedsbudget80
我们可以用下面的命令分析变量是否逃逸:
gorun-gcflags'-m-l'main.go
-m 其实是打印优化策略的语义,实际上最多总共可以用 4 个 -m,但是信息量较大,一般用 1 个就可以了;
-l 会禁用函数内联,在这里禁用掉内联能更好的观察逃逸情况,减少干扰
内联后堆栈信息还对不对
内联会将函数调用的过程抹掉,这会引入一个新的问题:代码的堆栈信息还能否保证。其实这一点不用担心,Golang 内部会为每个存在内联优化的 goroutine 维持一个内联树(inlining tree),该树可通过 -gcflags="-d pctab=pctoinline" 命令查看,Go在生成的代码中映射了内联函数。并且,也映射了行号。这张表被嵌入到了二进制文件中,所以在运行时可以得到准确的堆栈信息。
审核编辑:汤梓红
-
寄存器
+关注
关注
31文章
5343浏览量
120328 -
函数
+关注
关注
3文章
4331浏览量
62595 -
编译器
+关注
关注
1文章
1634浏览量
49128
原文标题:初探 Golang 内联
文章出处:【微信号:LinuxHub,微信公众号:Linux爱好者】欢迎添加关注!文章转载请注明出处。
发布评论请先 登录
相关推荐
评论