内联的策略和非内联性能对比

举报
码乐 发表于 2025/04/21 14:46:52 2025/04/21
【摘要】 1 简介语言的内联(inlining)机制在近年来逐渐演进,变得更智能、更复杂,在性能敏感场景中变得重要。图为 fractal一阶微分可视化如“叶函数内联”,“中间堆栈内联”, "启发式"内联都涉及 Go 的内联策略,但这些术语有点“半官方”,主要来源于对 Go 编译器源码(特别是 SSA 阶段)或社区讨论解读的约定俗成。 2、Go语言中内联机制和策略直到 Go 1.14,垃圾回收器还使用...

1 简介

语言的内联(inlining)机制在近年来逐渐演进,变得更智能、更复杂,在性能敏感场景中变得重要。

图为 fractal一阶微分可视化

如“叶函数内联”,“中间堆栈内联”, "启发式"内联都涉及 Go 的内联策略,但这些术语有点“半官方”,主要来源于对 Go 编译器源码(特别是 SSA 阶段)或社区讨论解读的约定俗成。

2、Go语言中内联机制和策略

直到 Go 1.14,垃圾回收器还使用堆栈检查前导码来中断,方法是将所有活动的协程堆栈设置为零,
迫使它们在下次进行函数调用时被困在运行时中。

在 Go 中函数方法只是一个具有预定义形式参数的函数,即接收者。
假设该方法不是通过接口调用的,调用 free 函数与调用方法的相对成本是相同的。

这个系统策略后面被一种机制所取代,该机制允许运行时暂停协程而无需等待它进行函数调用。

内联策略:

  • 叶函数内联(Leaf Inlining)

定义:

 指函数本身不调用其他函数(或只调用少量、可内联的函数),这类函数是内联的主要目标。

特点:

	简单、短小、调用开销可直接消除。

	编译器优先考虑这种函数做内联。
  • 非叶函数 / 中间堆栈内联(Mid-stack Inlining)

定义:

以前只有“叶函数”可以内联;但从 Go 1.12 开始,
引入了中间堆栈内联(mid-stack inlining),允许在非叶函数(即还有函数调用)中也进行内联。

关键意义:

允许在调用链中某些函数也内联,只要不会造成深度或复杂性爆炸。更大胆地打破“只能最末端内联”的限制。

限制条件:

内联预算不能爆表(复杂度预算、函数体大小等)

有递归、defer、recover、闭包等特性时可能禁止内联

示例:

func mid(a int) int {
	return add(a, 1) // add 是叶函数
}

func wrapper(a int) int {
    return mid(a) * 2
}

在 mid-stack inlining 启用时,add 和 mid 都有可能被内联进 wrapper 中。

  • 可内联判定标准(启发式)

Go 编译器会根据一套“启发式规则”来判断是否内联,主要参考:

    函数体是否过大(指令条数、AST节点数)

    是否含有复杂语义(defer、recover、闭包、循环递归)

    是否是泛型(Go 1.18+,对泛型函数内联支持不断增强)

    是否是导出函数(更谨慎)

    目标平台特性(有时跟具体架构有关)

可以用这个命令查看具体哪些函数被内联:

		go build -gcflags="-m" main.go

输出示例(来自 -gcflags="-m"):

	main.go:5:6: can inline Add
	main.go:10:7: inlining call to Add

说明:

		can inline Add:编译器认为可以内联

		inlining call to Add:实际做了内联

将调用堆栈底部的函数内联到其直接调用者的行为。

内联是一个递归过程,一旦函数被内联到其调用者中,编译器就可以将结果代码内联到其调用者中,

中间堆栈内联: 在调用堆栈中间内联函数时的另一种内联策略。

可以通过比较 with 和 without the annotation 的输出来自己检查这一点。

    go test -bench=. -gcflags=-S//go:noinline

可以自己用 flag 来检查。-gcflags=-d=ssa/prove/debug=on

  • Go 协程(goroutine)与内联函数的关系

每个 goroutine 有独立栈(初始约2KB,按需扩展),函数调用会在栈上分配帧。

  • 内联影响:

内联可以减少栈帧的创建:内联后代码嵌入调用者上下文中,因此不需要单独的栈帧。

节省栈空间 & 减少内存拷贝:尤其在高并发场景下(大量 goroutine),内联可以明显降低栈压力。

3 内联 vs 非内联Benchmark 示例对比性能

  1. 自动内联

Go 编译器会根据一些启发式规则决定是否内联(主要是函数体积、调用频率、是否递归等)。

  1. 强制禁止内联
    3
    使用编译指令:

     //go:noinline
     func myFunc() {
         ...
     }
    

可以防止函数被内联(用于调试或 Benchmark)。

  1. 查看哪些函数被内联?

使用编译器参数查看:

	go build -gcflags="-m" main.go

会输出:

	./main.go:10:6: can inline myFunc
	./main.go:20:7: inlining call to myFunc

是否内联的性能对比,函数非内联可以添加

    //go:noinline
  

注释来实现,从下一小节的Benchmark可以看出来内联可以带来性能提升。go源码里有很多//go:noinline注释

  • 一个简单例子:

      package inlinebench
    
      import "testing"
    
      func inlineAdd(a, b int) int {
          return a + b
      }
    
      //go:noinline
      func noinlineAdd(a, b int) int {
          return a + b
      }
    
      func BenchmarkInline(b *testing.B) {
          var r int
          for i := 0; i < b.N; i++ {
              r = inlineAdd(1, 2)
          }
          _ = r
      }
    
      func BenchmarkNoInline(b *testing.B) {
          var r int
          for i := 0; i < b.N; i++ {
              r = noinlineAdd(1, 2)
          }
          _ = r
      }
    

运行 Benchmark:

	go test -bench=. -benchmem

示例输出(不同机器略有不同):

	BenchmarkInline-8         1000000000   0.29 ns/op   0 B/op   0 allocs/op
	BenchmarkNoInline-8       250000000    4.92 ns/op   0 B/op   0 allocs/op

图形展示:

image.png

分析:

内联函数 inlineAdd:几乎免费,指令级优化后近似为一个加法操作。

非内联 noinlineAdd:仍需调用,开销在 4-5ns。

4 小结

可以使用指令来防止编译器内联 。如果我希望隔离内联的影响,比如常见的是使用注释。//go:noinline ,而不是使用 maxmax-gcflags=’-l -N’ 全局禁用优化。

从 Go 1.20 开始, 编译器开始对中间堆栈调用更积极地做 mid-stack inlining。 泛型函数逐步开始支持内联。内联 Budget 更智能,采用 SSA pass 分析。

内联策略的对照

	策略类型		特点							内联目标				是否启用默认
	叶内联		只内联叶函数					简单函数	✅ 			启用
	中间堆栈内联	支持调用链中非末端函数的内联	函数链优化			✅ 启用(Go 1.12+)
	控制注释		//go:noinline				显式禁止	🚫 			内联
	内联阈值控制	基于复杂度、大小				启发式				自动评估
【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。