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 中将支持实验性的 中间栈内联优化。
总之,内联是高性能编程的一种重要手段,每个函数调用都有开销:创建栈帧、读写寄存器,这些开销可以通过内联来避免。但是,对函数踢进行拷贝也会增加大二进制文件的大小。