逃逸分析

Go语言逃逸分析

堆内存与栈内存

程序会在两个地方为变量分配内存,一个是全局的堆空间用来动态分配内存,另一个是goroutine的栈空间,Go语言拥有自动的垃圾回收机制,我们不需要关心内存到底是分配在堆上还是栈上,但是从性能的角度出发,分配在栈空间还是堆空间差异较大。

在函数中申请一个对象,如果分配在栈上,函数执行完毕自动回收,如果分配在堆上,则在函数执行完毕后某个时间点进行垃圾回收。

在栈上分配或者回收内存的开销很低,只需要两个CPU指令,一个是Push,一个是Pop,Push代表分配内存,Pop代表释放内存。在栈上分配内存,消耗的仅是将内存分配到栈上的时间,内存的IO速度通常能够达到30GB/s,因此在栈上分配内存效率比较高。

在堆上分配内存,一个很大的额外开销是垃圾回收,Go语言使用的是清除标记算法,并在此基础上,使用了三色标记法和写屏障算法,提高了效率。

逃逸分析

Go语言中,堆内存是通过垃圾回收机制自动管理的,那么Go的编译器如何知道内存是分配在栈上还是堆上呢?编译器决定内存分配位置的方式,就叫逃逸分析。逃逸分析由编译器完成,作用于编译阶段。

指针逃逸

在函数中创建了一个对象,返回了这个对象的指针。这种情况下,函数虽然退出了,但是因为指针的存在,对象的内存不能随着函数的结束而回收,因此只能分配在堆上。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package main

import "fmt"

type Demo struct {
	name string
}

func createDemo(name string) *Demo {
	d := new(Demo)
	d.name = name
	return d
}
func main() {
	demo := createDemo("demo")
	fmt.Println(demo)
}
1
go run -gcflags "-m -l" Demo30.go
1
2
3
4
./Demo30.go:9:17: leaking param: name
./Demo30.go:10:10: new(Demo) escapes to heap
./Demo30.go:16:13: ... argument does not escape
&{demo}

new(Demo) escapes to heap说明new(Demo)逃逸到了堆上

Interface动态类型逃逸

空接口Interface{}可以表示任何类型,如果函数的参数为interface{},编译期无法确定具体类型,则会发生逃逸。

在上面的例子中

1
2
3
4
func main() {
	demo := createDemo("demo")
	fmt.Println(demo)
}

我们将main函数中的局部变量demo作为实际参数传给fmt.Println方法,

fmt.Println方法的实现如下

1
2
3
func Println(a ...any) (n int, err error) {
	return Fprintln(os.Stdout, a...)
}

参数类型为a…any即interface{}类型,因此发生了逃逸。

栈空间不足

超过一定大小的局部变量会逃逸到堆上,变量大小不确定,也会逃逸到堆上。

闭包

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package main

import "fmt"

func Increment() func() int {
	n := 0
	return func() int {
		n++
		return n
	}
}

func main() {
	increment := Increment()
	fmt.Println(increment())
	fmt.Println(increment())
}

Increment()返回值是一个闭包函数,该匿名函数访问了外部变量n,外部变量n将一直存在,直到increment被销毁。变量n的内存不能随着函数Increment()推出而被销毁,因此将会逃逸到堆上。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18

  Demo go run -gcflags=-m Demo31.go 
# command-line-arguments
./Demo31.go:5:6: can inline Increment
./Demo31.go:7:9: can inline Increment.func1
./Demo31.go:14:24: inlining call to Increment
./Demo31.go:7:9: can inline main.func1
./Demo31.go:15:23: inlining call to main.func1
./Demo31.go:15:13: inlining call to fmt.Println
./Demo31.go:16:23: inlining call to main.func1
./Demo31.go:16:13: inlining call to fmt.Println
./Demo31.go:6:2: moved to heap: n
./Demo31.go:7:9: func literal escapes to heap
./Demo31.go:14:24: func literal does not escape
./Demo31.go:15:13: ... argument does not escape
./Demo31.go:15:23: ~R0 escapes to heap
./Demo31.go:16:13: ... argument does not escape
./Demo31.go:16:23: ~R0 escapes to heap

一般情况下,对于需要修改原对象值,或占用内存比较大的结构体,选择传指针。对于只读的占用内存较小的结构体,直接传值能够获得更好的性能

Licensed under CC BY-NC-SA 4.0
comments powered by Disqus
Built with Hugo
主题 StackJimmy 设计