14

[译] Go语言inline内联的策略与限制 | yoko blog

 4 years ago
source link: https://pengrl.com/p/20028/?
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

[译] Go语言inline内联的策略与限制

2020-02-17 | Go
| 1.2k

0

本文基于Go 1.13

内联,就是将一个函数调用原地展开,替换成这个函数的实现。尽管这样做会增加编译后二进制文件的大小,但是它可以提高程序的性能。那么Go语言中,什么样的函数可以被内联呢?我们一起来看。

让我们从一个示例开始。下面这个程序的源码,分别编写在两个文件中,作用是对一组数字进行加或减:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
main.go

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
}

使用参数-gflags="-m"运行,可显示被内联的函数:

1
2
3
4
5
./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:12: inlining call to fmt.Printf

可以看到add方法被内联了。但是,为什么sum方法没有被内联呢?使用运行参数-gflags="-m -m"可以看到原因:

1
./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:

1

当一个函数的开销超过了这个预算,就无法内联。以下是一个更复杂的add函数对应的输出:

1
/op.go:3:6: cannot inline add: function too complex: cost 104 exceeds budget 80

当一个函数满足上面的所有条件,它就可以被内联。然而,依据以往的开发经验,内联优化可能带来一些其他问题。

举个例子,当发生panic时,开发者需要知道panic的准确堆栈信息,获取源码文件以及行号。那么问题来了,被内联的函数是否还有正确的堆栈信息呢?以下是一个包含了panic的内联方法:

1
2
3
4
5
6
7
func add(a, b float32) float32 {
if b < 0 {
panic(`Do not add negative number`)
}

return a+b
}

运行这个程序,我们可以看到panic显示了正确的源码行号,尽管它被内联了:

1
2
3
4
5
6
7
8
9
10
panic: Do not add negative number

goroutine 1 [running]:
main.add(...)
op.go:5
main.sum(0xc00007cf2c, 0x5, 0x5, 0xc00007cf20)
main.go:14 +0x80
main.main()
main.go:7 +0x59
exit status 2

这是因为,Go在内部维持了一份内联函数的映射关系。首先它会生成一个内联树,我们可以通过-gcflags="-d pctab=pctoinline"参数查看。以下是用sum方法的汇编代码构建出的内联树:

2

Go在生成的代码中映射了内联函数。并且,也映射了行号,可以通过-d pctab=pctoline参数查看。以下是sum方法的输出:

3

源码文件,可以通过-gcflags="-d pctab=pctofile"查看:

4

现在,我们得到了一张映射表:

5

这张表被嵌入到了二进制文件中,所以在运行时可以得到准确的堆栈信息。

内联带来的性能提升

内联是高性能编程的一种重要手段。每个函数调用都有开销:创建栈帧,读写寄存器,这些开销可以通过内联避免。但话说回来,对函数体进行拷贝也会增大二进制文件的大小。以下是内联与非内联时的一个benchmark对比:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
name                     old time/op    new time/op    delta
BinaryTree17-8 2.34s ± 2% 2.43s ± 3% +3.77%
Fannkuch11-8 2.21s ± 1% 2.26s ± 1% +2.01%
FmtFprintfEmpty-8 33.6ns ± 6% 35.2ns ± 3% +4.85%
FmtFprintfString-8 55.3ns ± 3% 62.8ns ± 1% +13.48%
FmtFprintfInt-8 63.1ns ± 3% 70.0ns ± 2% +11.04%
FmtFprintfIntInt-8 95.9ns ± 3% 102.3ns ± 3% +6.68%
FmtFprintfPrefixedInt-8 105ns ± 4% 111ns ± 1% +5.83%
FmtFprintfFloat-8 165ns ± 4% 175ns ± 1% +6.16%
FmtManyArgs-8 405ns ± 2% 427ns ± 0% +5.38%
GobDecode-8 4.69ms ± 2% 4.78ms ± 4% +1.77%
GobEncode-8 3.84ms ± 2% 3.93ms ± 3% ~
Gzip-8 210ms ± 3% 208ms ± 1% ~
Gunzip-8 28.1ms ± 7% 29.4ms ± 1% +4.69%
HTTPClientServer-8 70.0µs ± 2% 70.9µs ± 1% +1.21%
JSONEncode-8 7.28ms ± 5% 7.00ms ± 2% -3.91%
JSONDecode-8 33.9ms ± 3% 33.1ms ± 1% -2.32%
Mandelbrot200-8 3.74ms ± 0% 3.74ms ± 1% ~

内联的性能大概要好5~6%左右。

英文地址: https://medium.com/a-journey-with-go/go-inlining-strategy-limitation-6b6d7fc3b1be

本文完,作者yoko,尊重劳动人民成果,转载请注明原文出处: https://pengrl.com/p/20028/


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK