逃逸测试对比

返回引用逃逸的性能对比

使用指针可以避免拷贝,但是使用指针数很多情况下(如函数返回时)导致变量的作用域超出函数的调用栈,导致内存逃逸到堆内存上。

功能代码:

type S struct {
	a int64
	b string
	c float64
}

//返回结构体副本
func NewS() S {
	return S{
		100, "sword", 1.333,
	}
}

//返回结构体指针 (内存逃逸)
func NewSptr() *S {
	return &S{
		100, "sword", 1.333,
	}
}

性能测试:

//测试 返回结构体副本性能 
func BenchmarkNewS(b *testing.B) {
	var s S
	f, err := os.Create("stack.out")
	if err != nil {
		log.Panic(err)
	}
	defer f.Close()
	//开启追踪运行信息,并写入f
	if err = trace.Start(f); err != nil {
		panic(err)
	}

	//执行性能基准测试
	for i := 0; i < b.N; i++ {
		s = NewS()
		s.a = 10
		s.b = "aaa"
		s.c = 1.3333
	}

	trace.Stop()
	b.StopTimer()// StopTimer stops timing a test. This can be used to pause the timer
	_ = fmt.Sprintf("%v", s.a)
}

//返回结构体指针性能 (内存逃逸)
func BenchmarkNewSptr(b *testing.B) {
	var s *S
	f, err := os.Create("heap.out")
	if err != nil {
		log.Panic(err)
	}
	defer f.Close()
	//开启追踪运行信息,并写入f
	if err = trace.Start(f); err != nil {
		panic(err)
	}

	//执行性能基准测试
	for i := 0; i < b.N; i++ {
		s = NewSptr()
		s.a = 10
		s.b = "aaa"
		s.c = 1.3333
	}

	trace.Stop()
	b.StopTimer()// StopTimer stops timing a test. This can be used to pause the timer
	_ = fmt.Sprintf("%v", s.a)
}

运行基准测试go test -bench=. -benchmem -memprofile memprofile.out -cpuprofile profile.out 测试结果:

//BenchmarkNewS-2      	288926017	   4.17 ns/op	  0 B/op	       0 allocs/op
//BenchmarkNewSptr-2   	5580040	       212 ns/op	      32 B/op	   1 allocs/op
//从测试结果看,使用结构体副本比指针快很多,差了不止一个数量级

通过go tool trace 分析

go tool trace heap.out
go tool trace stack.out

# go tool trace heap.out
Parsing trace...
Splitting trace...
Opening browser. Trace viewer is listening on http://127.0.0.1:50790

# go tool trace stack.out
Parsing trace...
Splitting trace...
Opening browser. Trace viewer is listening on http://127.0.0.1:33595

通过浏览器访问: stack.out (使用结构体副本)

2020-03-21 22 23 42

heap (使用指针)

2020-03-21 22 22 02

通过对比发现,由于没有使用堆,因此没有垃圾收集器,也没有额外的 goroutine。使用指针迫使 go 编译器将变量逃逸到堆,由此增大了垃圾回收器的压力,产生了更多的系统调用。与栈相比,在堆上为变量分配内存是多么消耗资源。与在堆上分配内存并共享指针相比,代码在栈上分配结构体并复制副本要快得多。

堆内存回收代价大造成,要是这个内存块需要在不同函数中传递,并且内存块的生命周期比较长,那还是用指针比较好。

更新时间: