这个 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 则会打开内联,并启用更激进的内联策略。
单个函数级别:在函数定义前一行添加 //go:noinline ;
全局编译级别:可通过 -gcflags="-l" 选项全局禁用内联,与一个 -l 禁用内联相反,如果传递两个或两个以上的 -l 则会打开内联,并启用更激进的内联策略。
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.
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 104exceeds budget 80
我们可以用下面的命令分析变量是否逃逸:
gorun -gcflags '-m -l'main. go
-m 其实是打印优化策略的语义,实际上最多总共可以用 4 个 -m,但是信息量较大,一般用 1 个就可以了;
-l 会禁用函数内联,在这里禁用掉内联能更好的观察逃逸情况,减少干扰
-m 其实是打印优化策略的语义,实际上最多总共可以用 4 个 -m,但是信息量较大,一般用 1 个就可以了;
-l 会禁用函数内联,在这里禁用掉内联能更好的观察逃逸情况,减少干扰
内联会将函数调用的过程抹掉,这会引入一个新的问题: 代码的堆栈信息还能否保证。其实这一点不用担心,Golang 内部会为每个存在内联优化的 goroutine 维持一个内联树(inlining tree),该树可通过 -gcflags="-d pctab=pctoinline" 命令查看,Go在生成的代码中映射了内联函数。并且,也映射了行号。这张表被嵌入到了二进制文件中,所以在运行时可以得到准确的堆栈信息。
推荐大家感兴趣地话继续看看 Dave Cheney 相关的blog,对内联有更深入的讲解: dave.cheney.net/2020/05/02/…[3]
参考资料
Inlining optimisations in Go[4]
Go gcflags/ldflags 的说明[5]
[译] Go语言inline内联的策略与限制[6]
详解Go内联优化[7]
Inlining optimisations in Go[4]
Go gcflags/ldflags 的说明[5]
[译] Go语言inline内联的策略与限制[6]
详解Go内联优化[7]
[1]
github.com/golang/go/w…: https://github.com/golang/go/wiki/CompilerOptimizations#function-inlining
[2]
gocompiler.shizhz.me/8.-golang-b…: https://gocompiler.shizhz.me/8.-golang-bian-yi-qi-inline/8.4.3-nei-lian-cao-zuo
[3]
dave.cheney.net/2020/05/02/…: https://dave.cheney.net/2020/05/02/mid-stack-inlining-in-go
[4]
Inlining optimisations in Go: https://dave.cheney.net/2020/04/25/inlining-optimisations-in-go
[5]
Go gcflags/ldflags 的说明: https://www.bwangel.me/2022/01/12/go_gcflags/
[6]
[译] Go语言inline内联的策略与限制: https://www.pengrl.com/p/20028/
[7]
- EOF -
点击标题可跳转
1、 Golang 事件系统 Event Bus
2、 Golang 中的异步任务队列
3、 Go 眼中的文件系统是什么? io.FS
↓推荐关注↓
「Go 开发大全」参与维护一个非常全面的Go开源技术资源库。日常分享 Go, 云原生、k8s、Docker和微服务方面的技术文章和行业动态。关注后回复 Go 获取6万star的Go资源库