7

"b = &boy{}" vs "*b = boy{}" 谁不讲武德?golang 逃逸分析...

 3 years ago
source link: https://my.oschina.net/p1gd0g/blog/4816627
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.
neoserver,ios ssh client
"b = &boy{}" vs "*b = boy{}" 谁不讲武德?golang 逃逸分析入门

最近想要将 protobuf 变量和之前设计的数据对象整合起来,维护在内存中,以减少内存申请和 GC 的性能损耗。

feature or bug,gogoproto 解码疑惑

由于 gogoproto 在 unmarshal 时不保证输入和输出一致,作为结果的指针变量和输入的字节切片可能不一致(比如说,在 unmarshal slice 时没有 reset 操作)。我们需要对这个指针变量进行重置,pb 生成文件的 reset 实现方法如下。

func (m *Data) Reset() { *m = Data{} }

在看到 Data{} 时我陷入了疑惑,按我的理解,这一步是需要申请内存的。那么如此一来,我们在将某个 pb 变量抛入内存时不可避免的还是需要申请内存,这样本次的研发需求好像失去了意义。

我的第一反应是,这是 gogoproto 的问题,也许官方 go proto 不是这样的。可是重新生成后发现 reset 方法实现并没有什么区别。只不过官方 go proto 会在 unmarshal 时主动 reset

那么,难道一开始的方向就错了吗?啊头秃。

柳暗花明又一村

不死心的我开始看各种文档,包括 gogoproto 的各种插件,可惜并没有找到有用的内容。接着我又开始看官方 proto 文档。。。

这时我发现了一点蛛丝马迹。

在日常使用 protobuf 时,如果不复用旧的变量,我们一般会

  1. 声明指针变量,data := &pb.Data{}
  2. 解码,proto.Unmarshal(bytes, data)

显然,第一步是需要申请内存。而按照 go proto 的源码,unmarshal 时的 reset 操作又会申请一次内存,难道 Google 会允许这种性能损耗?

真的吗,我不信。

逃逸分析入门

想的太多,不如写个 benchmark 试一下。(小心 microbenchmark 的一些坑)

benchmark

package main

import (
	"testing"
)

type boy struct {
	name string
	age  int
}

var b1 = &boy{}
var b2 = &boy{}

func Benchmark_1(b *testing.B) {

	for i := 0; i < b.N; i++ {
		temp := &boy{}
		b1 = temp
	}
}

func Benchmark_2(b *testing.B) {

	for i := 0; i < b.N; i++ {
		temp := &boy{}
		*b1 = *temp
	}
}

func Benchmark_3(b *testing.B) {

	for i := 0; i < b.N; i++ {
		temp := &boy{}
		*b1 = *temp
		b2 = temp
	}
}

结果如下。

goos: linux
goarch: amd64
pkg: bible
Benchmark_1-4   	29142411	        42.2 ns/op	      32 B/op	       1 allocs/op
Benchmark_2-4   	1000000000	         0.711 ns/op	       0 B/op	       0 allocs/op
Benchmark_3-4   	28474614	        39.5 ns/op	      32 B/op	       1 allocs/op
PASS
ok  	bible	3.258s

结果是一目了然的,temp := &boy{} 确实没有重复申请内存。

到此,只需要逐个分析就好。首先打开编译报告看一下。

go build -gcflags "-m -m"

var b1 = &boy{}
//go:noinline
func main() {
	temp := &boy{}
	// &boy literal escapes to heap:
	//   flow: temp = &{storage for &boy literal}:
	//     from &boy literal (spill) at ./main.go:12:10
	//     from temp := &boy literal (assign) at ./main.go:12:7
	//   flow: {heap} = temp:
	//     from b1 = temp (assign) at ./main.go:13:5
	// &boy literal escapes to heap
	b1 = temp
}

新创建的 &boy{} 被全局变量引用,于是逃逸到堆上,成为动态变量,无法被重复利用。

var b1 = &boy{}
//go:noinline
func main() {
	temp := &boy{}
	// &boy literal does not escape
	*b1 = *temp
}

*b1 = *temp 仅仅是赋值操作,新创建的 &boy{} 没有被引用,留在栈上,后续被重复利用。

感谢 @Dash 同学的支持~

up-17b847306f12bc77897cd0dfdebe518cfd9.png

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK