go逃逸分析

概念

正常情况下对象在栈上分配内存,有些情况下会对象会在堆上分配内存,这就是逃逸。

go语言在一定程度消除了堆和栈的区别,go在编译的时候进行逃逸分析来决定一个对象放栈上还是放堆上,其中不逃逸的对象放栈上,可能逃逸的放堆上,分析的过程就是逃逸分析

逃逸分析时不止分析对象内存分配位置,也会做同步消除,如果你定义的对象的方法上有同步锁,但在运行时,却只有一个线程在访问,此时逃逸分析后的机器码,会去掉同步锁运行。

产生原因

  • 变量共享
    栈帧所在内存在栈帧结束后(return)变为不可访问区域,如果要栈帧中一些局部数据变量后扔有访问需求,需要将变量变为共享,具体的方法是返回局部变量的指针, 这样变量便会在堆上分配内存,后续可有效访问该块内存,达到共享的目的。

简而言之,任何时候,一个值被分享到函数栈帧范围之外,它都会在堆上被重新分配(逃逸)

拓展知识

对象在堆栈上有什么差异

  • 栈的分配比堆快,性能好
  • 在栈上分配对象最大的好处是可以减少gc的压力,不逃逸的对象分配在栈上,当函数返回时就回收了资源,不需要gc标记清除。
  • 保持在栈上的值,减少了 GC 的压力。但是需要存储,跟踪和维护不同的副本。将值放在堆上的指针,会增加 GC 的压力。然而,也有它的好处,只有一个值需要存储,跟踪和维护。最关键的是如何保持正确地、一致地以及均衡(开销)地使用。

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package main
type user struct {
name string
email string
}
func main() {
u1 := createUserV1()
u2 := createUserV2()
println("u1", &u1, "u2", &u2)
}
//go:noinline
func createUserV1() user {
u := user{
name: "Bill",
email: "bill@ardanlabs.com",
}
println("V1", &u)
return u
}
//go:noinline
func createUserV2() *user {
u := user{
name: "Bill",
email: "bill@ardanlabs.com",
}
println("V2", &u)
return &u
}

编译器报告

1
2
3
4
5
6
7
8
9
$ go build -gcflags "-m -m"
./escape_analysis.go:16:6: cannot inline createUserV1: marked go:noinline
./escape_analysis.go:27:6: cannot inline createUserV2: marked go:noinline
./escape_analysis.go:8:6: cannot inline main: function too complex: cost 133 exceeds budget 80
./escape_analysis.go:28:2: u escapes to heap:
./escape_analysis.go:28:2: flow: ~r0 = &u:
./escape_analysis.go:28:2: from &u (address-of) at ./escape_analysis.go:34:9
./escape_analysis.go:28:2: from return &u (return) at ./escape_analysis.go:34:2
./escape_analysis.go:28:2: moved to heap: u

参考资料

逃逸分析