golang inline

https://dave.cheney.net/2020/04/25/inlining-optimisations-in-go https://dave.cheney.net/2020/04/25/inlining-optimisations-in-go

Go语言 inline 内联的策略与限制 - 简书 https://www.jianshu.com/p/a6b0b5cdfc0e

内联函数和编译器对Go代码的优化 - 掘金 https://juejin.cn/post/6924888439577903117

 

在很多讲 Go 语言底层的技术资料和博客里都会提到内联函数这个名词,也有人把内联函数说成代码内联、函数展开、展开函数等等,其实想表达的都是 Go 语言编译器对函数调用的优化,编译器会把一些函数的调用直接替换成被调函数的函数体内的代码在调用处展开,以减少函数调用带来的时间消耗。它是Go语言编译器对代码进行优化的一个常用手段。

内联函数并不是 Go 语言编译器独有的,很多语言的编译器在编译代码时都会做内联函数优化,维基百科对内联函数的解释如下 (我把重点需要关注的信息特意进行了加粗):

计算机科学中,内联函数(有时称作在线函数编译时期展开函数)是一种编程语言结构,用来建议编译器对一些特殊函数进行内联扩展(有时称作在线扩展);也就是说建议编译器将指定的函数体插入并取代每一处调用该函数的地方(上下文),从而节省了每次调用函数带来的额外时间开支。但在选择使用内联函数时,必须在程序占用空间和程序执行效率之间进行权衡,因为过多的比较复杂的函数进行内联扩展将带来很大的存储资源开支。另外还需要特别注意的是对递归函数的内联扩展可能引起部分编译器的无穷编译。

Note: 内联优化一般用于能够快速执行的函数,因为在这种情况下函数调用的时间消耗显得更为突出,同时内联体量小的函数也不会明显增加编译后的执行文件占用的空间

Go 语言里的内联函数

举个例子来说,假设我有下面这样一个算两数之和的程序(不要紧张,不是算法题-两数之和...)

package main
import "fmt"
func main() {
  x := 20
  y := 5
  res := add(x, y)
  fmt.Println(res)
}

func add(x int, y int) int {
 return x + y
}
复制代码

上面的函数非常简单,add 函数对两个参数进行加和,编译器在编译上面的 Go 代码时会做内联优化,把 add 函数的函数体直接在调用处展开,等价于上面的 Go 代码是这么编写的。

package main
import "fmt"
func main() {
  x := 20
  y := 5
  // 内联函数, 或者叫把函数展开在调用处
  res := x + y 
  fmt.Println(res)
}

func add(x int, y int) int {
 return x + y
}
复制代码

告诉编译器不对函数进行内联

在源码编译的汇编代码里我们也看不到对 add 函数的调用,不过我们可以通过在 add 函数上增加一个特殊的注释来告诉编译器不要对它进行内联优化

// 注意下面这行注释,"//"后面不要有空格
//go:noinline
func add(x int, y int) int {
 return x + y
}
复制代码

怎么验证这个注释真实有效,能让编译器不对add函数做内联优化呢?我们可以用 go tool compile -S scratch.go 打印出的 Go 代码被编译成的汇编代码,在汇编代码里我们可以发现对add函数的调用。

0x0053 00083 (scratch.go:6)  CALL    "".add(SB)
复制代码

这也反向证明了,正常情况下 Go 编译器会对 add 函数进行内联优化。

让编译器告诉我们会对代码做哪些优化

除了分析编译后的汇编源码外,我们还可以通过给 go build 命令传递 -gcflags -m 参数

$ go build -gcflags --help
[.......]
// 传递 -m 选项会输出编译器对代码的优化
-m    print optimization decisions
复制代码

让编译器告诉我们它在编译 Go 代码对代码都做了哪些优化。

接下用 -gcflags -m 构建一下我们的小程序

$ go build -gcflags -m scratch.go

./scratch_16.go:10:6: can inline add
./scratch_16.go:6:12: inlining call to add
./scratch_16.go:7:13: inlining call to fmt.Println
./scratch_16.go:7:13: res escapes to heap
./scratch_16.go:7:13: main []interface {} literal does not escape
./scratch_16.go:7:13: io.Writer(os.Stdout) escapes to heap
复制代码

通过终端的输出可以了解到,编译器判断 add 函数可以进行内联,也对 add 函数进行了内联,除此之外还对fmt.Println 进行了内联优化。还有一个 io.Writer(os.Stdout) escapes to heap 的输出代表的是 io 对象逃逸到了堆上,堆逃逸是另外一种优化,在先前 Go内存管理系列的文章 -- Go内存管理之代码的逃逸分析 有详细说过。

哪些函数不会被内联

那么 Go 的编译器是不是会对所有的体量小,执行快的函数都会进行内联优化呢?我查查了资料发现 Go 在决策是否要对函数进行内联时有一个标准:

函数体内包含:闭包调用,select ,for ,defer,go 关键字的的函数不会进行内联。并且除了这些,还有其它的限制。当解析AST时,Go申请了80个节点作为内联的预算。每个节点都会消耗一个预算。比如,a = a + 1这行代码包含了5个节点:AS, NAME, ADD, NAME, LITERAL。以下是对应的SSA dump:

当一个函数的开销超过了这个预算,就无法内联

以上描述翻译自文章: medium.com/a-journey-w…

总结

内联是高性能编程的一种重要手段。每个函数调用都有开销:创建栈帧,读写寄存器,这些开销可以通过内联避免,对性能的提升大概在5~6%左右。但内联对函数体进行拷贝也会增大编译后二进制文件的大小,不过好在使用Go语言编程时,编译器会帮助我们决策哪些函数可以内联,大大降低了使用者的心智负担 。

 
 
 
[译] Go语言inline内联的策略与限制_go inline_DreamCatcher的博客-CSDN博客 https://blog.csdn.net/hero_java/article/details/114029057

 
1、什么是内联

内联,就是将一个函数调用原地展开,替换成这个函数的实现。尽管这样做会增加编译后二进制文件的大小,但是却可以提高程序的性能。

在 Go 中,函数调用有固定的开销;栈和抢占检查。

硬件分支预测器改善了其中的一些功能,但就功能大小和时钟周期而言,这仍然是一个成本。

内联是避免这些成本的经典优化方法。

内联只对叶子函数有效,叶子函数是不调用其他函数的。这样做的理由是:

如果你的函数做了很多工作,那么前序开销可以忽略不计。
另一方面,小函数为相对较少的有用工作付出固定的开销。这些是内联目标的功能,因为它们最受益。
2、规则

编写两个go文件,分别是main.go和op.go,作用就是对一组数字进行加和减

//main.go
import "fmt"

func main() {
n := []float32{120.4, -46.7, 32.50, 34.65, -67.45}
fmt.Printf("The total is %.02f\n",sum(n))
}

func sum(s []float32) float32 {
var t float32
for _,v := range s {
if t < 0 {
t = add(t,v)
} else {
t = sub(t,v)
}
}
return t
}

//op.go
func add(a,b float32) float32 {
return a + b
}

func sub(a,b float32) float32{
return a - b
}
使用以下命令来build

go build -gcflags=-m main.go op.go
运行结果如下:

# command-line-arguments
.\op.go:3:6: can inline add
.\op.go:7:6: can inline sub
.\main.go:16:11: inlining call to sub
.\main.go:14:11: inlining call to add
.\main.go:7:13: inlining call to fmt.Printf
.\main.go:10:10: s does not escape
.\main.go:6:17: []float32 literal does not escape
.\main.go:7:40: sum(n) escapes to heap
.\main.go:7:13: []interface {} literal does not escape
<autogenerated>:1: .this does not escape
<autogenerated>:1: .this does not escape
可以看到方法add、sub被内联了,但是sum方法没有被内联,但是sum方法逃逸到heap 上了

可以通过以下命令来查看,sum方法为何没有被内联

go build -gcflags="-m -m" main.go op.go
运行结果如下:

# command-line-arguments
.\op.go:3:6: can inline add with cost 4 as: func(float32, float32) float32 { return a + b }
.\op.go:7:6: can inline sub with cost 4 as: func(float32, float32) float32 { return a - b }
.\main.go:10:6: cannot inline sum: unhandled op RANGE
.\main.go:16:11: inlining call to sub func(float32, float32) float32 { return a - b }
.\main.go:14:11: inlining call to add func(float32, float32) float32 { return a + b }
.\main.go:5:6: cannot inline main: function too complex: cost 148 exceeds budget 80
.\main.go:7:13: inlining call to fmt.Printf func(string, ...interface {}) (int, error) { var fmt..autotmp_4 int; fmt..autotmp_4 = <N>; var fmt..autotmp_5 error; fm
t..autotmp_5 = <N>; fmt..autotmp_4, fmt..autotmp_5 = fmt.Fprintf(io.Writer(os.Stdout), fmt.format, fmt.a...); return fmt..autotmp_4, fmt..autotmp_5 }
.\main.go:10:10: s does not escape
.\main.go:7:40: sum(n) escapes to heap:
.\main.go:7:40: flow: ~arg1 = &{storage for sum(n)}:
.\main.go:7:40: from sum(n) (spill) at .\main.go:7:40
.\main.go:7:40: from fmt.format, ~arg1 = <N> (assign-pair) at .\main.go:7:13
.\main.go:7:40: flow: {storage for []interface {} literal} = ~arg1:
.\main.go:7:40: from []interface {} literal (slice-literal-element) at .\main.go:7:13
.\main.go:7:40: flow: fmt.a = &{storage for []interface {} literal}:
.\main.go:7:40: from []interface {} literal (spill) at .\main.go:7:13
.\main.go:7:40: from fmt.a = []interface {} literal (assign) at .\main.go:7:13
.\main.go:7:40: flow: {heap} = *fmt.a:
.\main.go:7:40: from fmt.Fprintf(io.Writer(os.Stdout), fmt.format, fmt.a...) (call parameter) at .\main.go:7:13
.\main.go:6:17: []float32 literal does not escape
.\main.go:7:40: sum(n) escapes to heap
.\main.go:7:13: []interface {} literal does not escape
<autogenerated>:1: .this does not escape
<autogenerated>:1: .this does not escape
关键在于这句输出:

.\main.go:10:6: cannot inline sum: unhandled op RANGE

Go不会内联包含循环的方法。实际上,包含以下内容的方法都不会被内联:

闭包调用、select、for、defer、go关键字创建的协程。

并且除了这些,还有其他的限制。

当解析AST时,Go申请了80个节点作为内联的预算,每个节点都会消耗一个预算。

比如:a = a +1 这行代码包含了5个节点:

AS,NAME,ADD,NAME,LITERAL,以下是对应的SSA dump:

 

当一个函数的开销超过了预算,就无法内联,例如:

.\main.go:5:6: cannot inline main: function too complex: cost 148 exceeds budget 80

但是严重的内联会使得堆栈信息更加难以追踪。

3、挑战

当发生panic时,开发人员需要知道panic的准确堆栈信息,获取源码文件以及行号。那么问题来了,

被内联的函数是否还有正确的堆栈信息吗?

下面是一个包含panic的内联方法:

func add(a,b float32) float32 {
if b < 0 {
panic(`Do not add negative number`)
}
return a + b
}
运行这个程序,可以看到panic显示了正确的源码行号,尽管它被内联了:

 

这是因为,Go在内部维持了一份内联函数的映射关系,首先它会生成一个内联树,

可以通过以下命令查看

go build -gcflags="-d pctab=pctoinline" main.go op.go
输出的部分结果如下:

-- inlining tree for "".sum:
0 | -1 | "".add (E:\goproject\awesomeProject\interview\inlining\main.go:14:11) pc=83
1 | -1 | "".sub (E:\goproject\awesomeProject\interview\inlining\main.go:16:11) pc=89
--
funcpctab "".add [valfunc=pctoinline]
0 -1 00000 (E:\goproject\awesomeProject\interview\inlining\op.go:3) TEXT "".add(SB), ABIInternal, $24-16
0 00000 (E:\goproject\awesomeProject\interview\inlining\op.go:3) TEXT "".add(SB), ABIInternal, $24-16
0 -1 00000 (E:\goproject\awesomeProject\interview\inlining\op.go:3) MOVQ TLS, CX
9 00009 (E:\goproject\awesomeProject\interview\inlining\op.go:3) PCDATA $0, $-2
9 00009 (E:\goproject\awesomeProject\interview\inlining\op.go:3) MOVQ (CX)(TLS*2), CX
10 00016 (E:\goproject\awesomeProject\interview\inlining\op.go:3) PCDATA $0, $-1
10 00016 (E:\goproject\awesomeProject\interview\inlining\op.go:3) CMPQ SP, 16(CX)
14 00020 (E:\goproject\awesomeProject\interview\inlining\op.go:3) PCDATA $0, $-2
14 00020 (E:\goproject\awesomeProject\interview\inlining\op.go:3) JLS 105
16 00022 (E:\goproject\awesomeProject\interview\inlining\op.go:3) PCDATA $0, $-1
16 00022 (E:\goproject\awesomeProject\interview\inlining\op.go:3) SUBQ $24, SP
1a 00026 (E:\goproject\awesomeProject\interview\inlining\op.go:3) MOVQ BP, 16(SP)
1f 00031 (E:\goproject\awesomeProject\interview\inlining\op.go:3) LEAQ 16(SP), BP
24 00036 (E:\goproject\awesomeProject\interview\inlining\op.go:3) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
24 00036 (E:\goproject\awesomeProject\interview\inlining\op.go:3) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
24 00036 (E:\goproject\awesomeProject\interview\inlining\op.go:4) XORPS X0, X0
27 00039 (E:\goproject\awesomeProject\interview\inlining\op.go:4) MOVSS "".b+36(SP), X1
2d 00045 (E:\goproject\awesomeProject\interview\inlining\op.go:4) UCOMISS X1, X0
30 00048 (E:\goproject\awesomeProject\interview\inlining\op.go:4) JHI 76
32 00050 (E:\goproject\awesomeProject\interview\inlining\op.go:7) MOVSS "".a+32(SP), X0
38 00056 (E:\goproject\awesomeProject\interview\inlining\op.go:7) ADDSS X1, X0
3c 00060 (E:\goproject\awesomeProject\interview\inlining\op.go:7) MOVSS X0, "".~r2+40(SP)
42 00066 (E:\goproject\awesomeProject\interview\inlining\op.go:7) MOVQ 16(SP), BP
47 00071 (E:\goproject\awesomeProject\interview\inlining\op.go:7) ADDQ $24, SP
4b 00075 (E:\goproject\awesomeProject\interview\inlining\op.go:7) RET
4c 00076 (E:\goproject\awesomeProject\interview\inlining\op.go:5) LEAQ type.string(SB), AX
53 00083 (E:\goproject\awesomeProject\interview\inlining\op.go:5) MOVQ AX, (SP)
57 00087 (E:\goproject\awesomeProject\interview\inlining\op.go:5) LEAQ ""..stmp_1(SB), AX
5e 00094 (E:\goproject\awesomeProject\interview\inlining\op.go:5) MOVQ AX, 8(SP)
63 00099 (E:\goproject\awesomeProject\interview\inlining\op.go:5) PCDATA $1, $0
63 00099 (E:\goproject\awesomeProject\interview\inlining\op.go:5) CALL runtime.gopanic(SB)
68 00104 (E:\goproject\awesomeProject\interview\inlining\op.go:5) XCHGL AX, AX
69 00105 (E:\goproject\awesomeProject\interview\inlining\op.go:5) NOP
69 00105 (E:\goproject\awesomeProject\interview\inlining\op.go:3) PCDATA $1, $-1
69 00105 (E:\goproject\awesomeProject\interview\inlining\op.go:3) PCDATA $0, $-2
69 00105 (E:\goproject\awesomeProject\interview\inlining\op.go:3) CALL runtime.morestack_noctxt(SB)
6e 00110 (E:\goproject\awesomeProject\interview\inlining\op.go:3) PCDATA $0, $-1
6e 00110 (E:\goproject\awesomeProject\interview\inlining\op.go:3) JMP 0
70 done
wrote 3 bytes to 0xc000437e68
00 70 00
funcpctab "".sub [valfunc=pctoinline]
0 -1 00000 (E:\goproject\awesomeProject\interview\inlining\op.go:10) TEXT "".sub(SB), NOSPLIT|ABIInternal, $0-16
0 00000 (E:\goproject\awesomeProject\interview\inlining\op.go:10) TEXT "".sub(SB), NOSPLIT|ABIInternal, $0-16
0 00000 (E:\goproject\awesomeProject\interview\inlining\op.go:10) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0 00000 (E:\goproject\awesomeProject\interview\inlining\op.go:10) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0 -1 00000 (E:\goproject\awesomeProject\interview\inlining\op.go:11) MOVSS "".a+8(SP), X0
6 00006 (E:\goproject\awesomeProject\interview\inlining\op.go:11) MOVSS "".b+12(SP), X1
c 00012 (E:\goproject\awesomeProject\interview\inlining\op.go:11) SUBSS X1, X0
10 00016 (E:\goproject\awesomeProject\interview\inlining\op.go:11) MOVSS X0, "".~r2+16(SP)
16 00022 (E:\goproject\awesomeProject\interview\inlining\op.go:11) RET
17 done
wrote 3 bytes to 0xc000084c68
00 17 00

Go 在生成的代码中映射了内联函数,并且,也映射了行号,可以通过-d pctab=pctoline(可查看源文件行号) 或者是 -gcflags="-d pctab=pctofile"参数查看

 

此内联函数的映射关系会形成一张映射表,并嵌入到了二进制文件中,所以在运行时可以得到准确的堆栈信息。得到的映射表,可能以以下形式:

 

4、调整内联级别

调整内联级别

使用-gcflags=-l标识调整内联级别。有些令人困惑的是,传递一个-l将禁用内联,两个或两个以上将在更激进的设置中启用内联。

-gcflags=-l,禁用内联。
什么都不做,常规的内联
-gcflags='-l -l' 内联级别2,更积极,可能更快,可能会制作更大的二进制文件。
-gcflags='-l -l -l' 内联级别3,再次更加激进,二进制文件肯定更大,也许更快,但也许会有 bug。
-gcflags=-l=4 (4个 -l) 在 Go 1.11 中将支持实验性的 中间栈内联优化。
总之,内联是高性能编程的一种重要手段,每个函数调用都有开销:创建栈帧、读写寄存器,这些开销可以通过内联来避免。但是,对函数踢进行拷贝也会增加大二进制文件的大小。

 
 
 
 

 

 

posted @ 2023-04-20 01:31  papering  阅读(207)  评论(0编辑  收藏  举报