Go 高性能之内联优化

概述 #

内联 (inline) 就是 将函数的调用代码替换为函数的具体实现代码 (编译器实现),程序运行过程中直接执行内联后展开的代码, 节省了函数调用的开销(创建栈帧、读写寄存器、栈溢出检测等),可以提升性能,但是带来的一个问题是编译后的二进制文件体积增大。

接下来,我们先通过一个示例来了解下 内联

示例 #

// 原代码:
package main

func max(x, y int) int {
	if x > y {
		return x
	}
	return y
}

func main() {
	z := max(1, 2)
	println(z)
}

内联后代码猜测 #

上面的代码,内联之后展开成类似下面的代码:

package main

func main() {
	var z int
	if 1 > 2 {
		z = 1
	} else {
		z = 2
	}
	println(z)
}

当然,因为这个程序实在过于简单,编译器可以直接优化为:

// 最终优化后代码
package main

func main() {
	println(2)
}

直接优化为一行代码, 编译器真的有这么强大吗? 接下来我们通过构建和反汇编代码一起来验证一下。

内联后反汇编代码 #

原代码 进行内联构建:

# 构建时开启内联优化
$ go build -gcflags -m main.go

# 输出如下
# command-line-arguments
./main.go:3:6: can inline max
./main.go:10:6: can inline main
./main.go:12:10: inlining call to max

查看内联构建后的反汇编代码:

$ go tool objdump -s "main.main" main | grep CALL

# 输出如下
main.go:4             0x457c14                e84776fdff              CALL runtime.printlock(SB)
main.go:4             0x457c20                e83b7dfdff              CALL runtime.printint(SB)
main.go:4             0x457c25                e89678fdff              CALL runtime.printnl(SB)
main.go:4             0x457c2a                e8b176fdff              CALL runtime.printunlock(SB)
main.go:3             0x457c39                e802cdffff              CALL runtime.morestack_noctxt.abi0(SB)

验证猜测 #

最终优化后代码 进行构建:

# 构建时开启内联优化
$ go build main.go

查看内联构建后的反汇编代码:

$ go tool objdump -s "main.main" main | grep CALL

# 输出如下
main.go:4             0x457c14                e84776fdff              CALL runtime.printlock(SB)
main.go:4             0x457c20                e83b7dfdff              CALL runtime.printint(SB)
main.go:4             0x457c25                e89678fdff              CALL runtime.printnl(SB)
main.go:4             0x457c2a                e8b176fdff              CALL runtime.printunlock(SB)
main.go:3             0x457c39                e802cdffff              CALL runtime.morestack_noctxt.abi0(SB)

最后,经过两次生成的反汇编代码对比,结果是一样的,这验证了我们的猜想,编译器的优化确实非常强大

内联条件 #

并不是所有条件下编译器都会内联,以下场景不会内联 (可能随着 Go 版本的变化而变化):

  • for
  • select
  • defer
  • recover
  • go
  • 闭包
  • 不能以 go:noinline 或 go:unitptrescapes 作为编译指令

除此之外,还有其他的限制,当解析 AST 时,Go 申请了 80 个节点 作为内联的数量上限,每个节点都会消耗一个预算。 比如,a = a + 1 包含了5个节点:AS, NAME, ADD, NAME, LITERAL

内联与非内联性能差异 #

测试代码 #

package performance

import (
	"strconv"
	"testing"
)

type person struct {
	name string
	age  int
}

func Benchmark_Compare(b *testing.B) {
	b.StopTimer()

	// 初始化数据
	persons := make([]*person, 100)
	for i := range persons {
		persons[i] = &person{
			name: strconv.Itoa(i),
			age:  i,
		}
	}

	b.StartTimer()

	for i := 0; i < b.N; i++ {
		for j := range persons {
			_ = persons[j].name
		}
	}
}

内联测试 #

运行测试,并将基准测试结果写入文件:

# 运行 10000000 次, 统计内存分配
$ go test -run='^$' -bench=. -count=1 -benchtime=10000000x -benchmem > inline.txt

非内联测试 #

运行测试,并将基准测试结果写入文件:

# 运行 10000000 次, 统计内存分配
$ go test -gcflags "-N -l" -run='^$' -bench=. -count=1 -benchtime=10000000x -benchmem > noinline.txt

使用 benchstat 比较差异 #

$ benchstat -alpha=100 inline.txt noinline.txt

# 输出如下
name               old time/op    new time/op    delta
_Compare-8    63.3ns ± 0%   180.2ns ± 0%  +184.81%  (p=1.000 n=1+1)

name               old alloc/op   new alloc/op   delta
_Compare-8     0.00B          0.00B           ~     (all equal)

name               old allocs/op  new allocs/op  delta
_Compare-8      0.00           0.00           ~     (all equal)

从输出的结果中可以看到,默认的 内联优化 性能比 非内联优化 的性能提升了将近 2 倍

更激进的内联 #

gcflags 参数可以设置多个 -l 选项,每多加 1 个,表示编译器将采用更加激进的内联方式,同时也可能生成更大的二进制文件。

  • -gcflags='-l -l' 2 级内联
  • -gcflags='-l -l -l' 3 级内联
  • gcflags=-l=4 4 级别内联

禁用内联 #

大多数情况下,不需要禁用内联设置,这里提到的禁止方法暂且作为备忘。

单个函数禁用 #

加上 //go:noinline 编译指令,如下代码所示:

//go:noinline
func max(x, y int) int {
if x > y {
return x
}
return y
}

全局禁用 #

# 编译时禁用内联优化
$ go build -gcflags="-l" main.go

# 同时禁止编译器优化和内联优化
$ go build -gcflags="-N -l" main.go

小结 #

默认的内联优化在不断优化和完善,这意味我们无需额外配置,然后定期升级 Go 版本,就可以享受到内联带来的性能提升红利。

扩展阅读 #

转载申请

本作品采用 知识共享署名 4.0 国际许可协议 进行许可,转载时请注明原文链接,图片在使用时请保留全部内容,商业转载请联系作者获得授权。

© 蛮荆 | 陕公网安备 61011302001681 号 | 陕ICP备2023004378号-1 | Rendered by Hugo