这个 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资源库