逃逸分析
逃逸分析是指由编译器决定内存分配的位置,不需要程序员指定。在函数中申请一个新的对象:
- 如果分配在栈中,则函数执行结束后可自动将内存回收
- 如果分配在堆中,则函数执行结束后可交给GC(垃圾回收)处理
有了逃逸分析,返回函数局部变量变得可能。除此之外,逃逸分析还跟闭包息息相关,了解哪些场景下对象会逃逸至关重要。
1. 逃逸策略
在函数中申请新的对象时,编译器会根据该对象是否被函数外部引用来决定是否逃逸:
- 如果函数外部没有引用,则优先放到栈中
- 如果函数外部存在引用,则必定放在堆中
对于仅在函数内部使用的变量,也有可能放到堆中
,比如内存过大超过栈的存储能力。
2. 逃逸场景
1)指针逃逸
Go可以返回局部变量指针,这是一个典型的变量逃逸案例。示例代码如下:
package main
type Student struct {
Name string
Age int
}
func StudentRegister(name string, age int) *Student {
s := new(Student)
s.Name = name
s.Age = age
return s
}
func main() {
StudentRegister("Jim", 18)
}
函数 StudentRegister() 内部的s为局部变量,其值通过函数值返回,s本身为一个指针,其指向的内存地址不会是栈,而是堆,这就是典型的逃逸案例。
通过编译参数 -gcflags=-m
可以查看编译过程中的逃逸分析过程:
PS D:\Go\workspace\src\lekou> go build -gcflags=-m
# lekou
./main.go:8:6: can inline StudentRegister
./main.go:14:6: can inline main
./main.go:15:17: inlining call to StudentRegister
./main.go:8:22: leaking param: name
./main.go:9:10: new(Student) escapes to heap
./main.go:15:17: new(Student) does not escape
PS D:\Go\workspace\src\lekou>
在 StudentRegister() 函数中,代码第9行显示 “escapes to heap” ,表示该行内存分配发生了逃逸现象。
2)栈空间不足逃逸
当栈空间不足以存放当前对象或无法判断当前切片长度时会将对象分配到堆中。示例代码:
package main
func Slice() {
s1 := make([]int, 1000, 1000)
s2 := make([]int, 10000, 10000)
for index, _ := range s1 {
s1[index] = index
}
for index, _ := range s2 {
s2[index] = index
}
}
func main() {
Slice()
}
s1的长度为1000,s2的长度为10000。查看编译提示,如下:
PS D:\Go\workspace\src\lekou> go build -gcflags=-m
# lekou
./main.go:3:6: can inline Slice
./main.go:14:6: can inline main
./main.go:15:7: inlining call to Slice
./main.go:4:12: make([]int, 1000, 1000) does not escape // s1
./main.go:5:12: make([]int, 10000, 10000) escapes to heap // s2
./main.go:15:7: make([]int, 1000, 1000) does not escape
./main.go:15:7: make([]int, 10000, 10000) escapes to heap
可以发现s1没有发生逃逸,s2发生逃逸。
3)动态类型逃逸
很多函数的参数为 interface 类型,比如 fmt.Println(a …interface{}),编译期间很难确定其参数的具体类型,也会产生逃逸,如以下代码所示。
package main
import "fmt"
func main() {
s := "Hello World!"
fmt.Println(s)
}
上述代码中的 s 变量只是一个 string类型变量,调用 fmt.Println()时会产生逃逸:
PS D:\Go\workspace\src\lekou> go build -gcflags=-m
# lekou
./main.go:7:13: inlining call to fmt.Println
./main.go:7:13: ... argument does not escape
./main.go:7:14: s escapes to heap
4)闭包引用对象逃逸
某著名的开源框架实现了某个返回Fibonacci数列的函数:
func Fibonacci() func() int {
a, b := 0, 1
return func() int {
a, b = b, a+b
return a
}
}
该函数返回一个闭包,闭包引用了函数的局部变量 a 和 b,使用时通过该函数获取闭包,然后每次执行闭包都会依次输出 Fibonacci 数列。完整的示例程序如下:
package main
import "fmt"
func Fibonacci() func() int {
a, b := 0, 1
return func() int {
a, b = b, a+b
return a
}
}
func main() {
f := Fibonacci()
for i := 0; i < 10; i++ {
fmt.Printf("Fibonacci:%d\n", f())
}
}
上述代码通过 Fibonacci()获取一个闭包,每次执行闭包就会打印一个 Fibonacci 数值。输出如下:
C:\Users\666\AppData\Local\Temp\GoLand\___293go_build_lekou_.exe
Fibonacci:1
Fibonacci:1
Fibonacci:2
Fibonacci:3
Fibonacci:5
Fibonacci:8
Fibonacci:13
Fibonacci:21
Fibonacci:34
Fibonacci:55
Process finished with the exit code 0
Fibonacci() 函数中原本属于局部变量的 a 和 b 由于闭包的引用,不得不将二者放到堆中,以致产生逃逸:
PS D:\Go\workspace\src\lekou> go build -gcflags=-m
# lekou
./main.go:3:6: can inline Slice
./main.go:10:6: can inline main
./main.go:11:7: inlining call to Slice
./main.go:4:11: make([]int, 1000, 1000) does not escape
./main.go:14:16: inlining call to Fibonacci
./main.go:7:9: can inline main.Fibonacci.func1
./main.go:17:35: inlining call to main.Fibonacci.func1
./main.go:17:13: inlining call to fmt.Printf
./main.go:6:2: moved to heap: a
./main.go:6:5: moved to heap: b
./main.go:7:9: func literal escapes to heap
./main.go:14:16: func literal does not escape
./main.go:17:13: ... argument does not escape
./main.go:17:35: ~r0 escapes to heap
3. 小结
- 栈上分配内存比在堆中分配内存有更高的效率
- 栈上分配内存不需要GC处理
- 对上分配的内存使用完毕会交给GC处理
- 逃逸分析的目的是决定分配地址是栈还是堆
- 逃逸分析在编译阶段完成
4. 问题
思考一下这个问题:函数传递指针真的比传值的效率高吗?
我们知道传递指针可以减少底层值的复制,可以提高效率,但是如果复制的数据量小,由于指针传递会产生逃逸,则可能会使用堆,也可能增加GC的负担,所以传递指针不一定是高效的。