0x00 内存管理

 go变量声明存在两部分:

  • Heap: 全局存储空间, 由runtime来管理,这部分区域是可以共享的, 生命周期由gc来管理, 对象可以被其他函数访问到(通过指针方式)
  • Stack: 局部变量存储,生命周期由对应的函数来管理,并且只能被这个函数内部来访问到,没有gc。

一般放在stack里面性能要比heap速度快的多得多,所以尽可能的将对象放到stack里面
而go逃逸是在编译阶段分析对象,当一个在stack里面声明的一个variable试图被分配到heap里面,这就发生了逃逸,逃逸分析的目的是为了尽可能的将对象分配在stack里面,而不是heap上,这两者性能相差非常大
分配在stack里面的variable是由function本身来管理,而分配在heap上面的对象是由runtime来管理,这个里面还涉及到一些列的管理机制

测试例子
test
测试结果
result

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import "testing"

type T struct {
X [10 * 1000 * 1000]byte // 最大只能分配10mb, 并且当试图生命一个超过10MB的对象的时候,也会发生逃逸现象
}

var global interface{}

func BenchmarkAllocOnHeap(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
global = &T{}
}
}

func BenchmarkAllocOnStack(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
local := T{}
_ = local
}
}
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
➜  demo go test -cpu 1,2,4,8,16 -bench=. demo_test.go
goos: darwin
goarch: amd64
cpu: Intel(R) Core(TM) i7-4770HQ CPU @ 2.20GHz
BenchmarkAllocOnHeap 1467 815408 ns/op 10002432 B/op 1 allocs/op
BenchmarkAllocOnHeap-2 1532 810348 ns/op 10002432 B/op 1 allocs/op
BenchmarkAllocOnHeap-4 1410 834213 ns/op 10002434 B/op 1 allocs/op
BenchmarkAllocOnHeap-16 1410 862156 ns/op 10002435 B/op 1 allocs/op
BenchmarkAllocOnStack 1000000000 0.3225 ns/op 0 B/op 0 allocs/op
BenchmarkAllocOnStack-2 1000000000 0.3128 ns/op 0 B/op 0 allocs/op
BenchmarkAllocOnStack-4 1000000000 0.3148 ns/op 0 B/op 0 allocs/op
BenchmarkAllocOnStack-8 1000000000 0.3148 ns/op 0 B/op 0 allocs/op
BenchmarkAllocOnStack-16 1000000000 0.3125 ns/op 0 B/op 0 allocs/op
PASS
ok command-line-arguments 10.722s
➜ demo vim demo_test.go
# 下面例子是例子设置了X为12m大小, 这个里试图分配在stack里面的对象发生了逃逸
➜ demo go test -cpu 1,2,4,8,16 -bench=. demo_test.go
goos: darwin
goarch: amd64
cpu: Intel(R) Core(TM) i7-4770HQ CPU @ 2.20GHz
BenchmarkAllocOnHeap 1306 916347 ns/op 12001280 B/op 1 allocs/op
BenchmarkAllocOnHeap-2 1254 963093 ns/op 12001280 B/op 1 allocs/op
BenchmarkAllocOnHeap-4 1263 957514 ns/op 12001282 B/op 1 allocs/op
BenchmarkAllocOnHeap-8 1236 980502 ns/op 12001287 B/op 1 allocs/op
BenchmarkAllocOnHeap-16 1168 982603 ns/op 12001283 B/op 1 allocs/op
BenchmarkAllocOnStack 1218 992638 ns/op 12001280 B/op 1 allocs/op
BenchmarkAllocOnStack-2 1161 980680 ns/op 12001280 B/op 1 allocs/op
BenchmarkAllocOnStack-4 1218 950208 ns/op 12001286 B/op 1 allocs/op
BenchmarkAllocOnStack-8 1164 996705 ns/op 12001290 B/op 1 allocs/op
BenchmarkAllocOnStack-16 1119 1018326 ns/op 12001286 B/op 1 allocs/op
PASS
ok command-line-arguments 14.127s

0x01 go逃逸分析

 go逃逸分析是在编译阶段分析,它会试图将对象尽可能的保留在stack里面,如果variable分配在heap上面,那么就会发生逃逸.
 逃逸机制核心:一个对象处于heap还是stack和它是什么类型,以及是如何被创建出来的没有任何关系,只取决于它是如何被共享的/使用的。
 逃逸发生的条件:

  • 它的地址可以别别的对象(例如a)通过&取地址符号获取到
  • 对象a本身已经发生了逃逸

    当满足上面两个条件的时候就会发生逃逸

1
2
3
4
# 本质上是利用有向图和打分来判断对象是否要移动到heap上面
1. construct locations
2. construct edges
3. Anylyze the build graph

0x02 go逃逸的几种特殊规则

0x00 大对象
  • 显示声明的对象超过10m——发生逃逸
  • 通过new/make 声明的对象超过64kb ——发生逃逸

case
result

0x01 slice

 如果capacity是在编译期间无法确定的值,在这种情况下会发生逃逸
case
result

0x02 map

map是一个比较特殊的例子

  • map本身是存放在stack里面的
  • 任何变量只要被map的key或者value引用到(by reference/by pointer),那么这个变量就会发生逃逸, 不管map本身是不是已经逃逸.

case
result

0x03 返回值

 如果返回值是指针类型,那么他们就会发生逃逸,如果返回类型是map或者slice,那么他们也会发生逃逸

case
result

0x04 输入参数

 如果输入形参有泄漏的情况,那么实参数就会发生逃逸

一般input要不return安全的多,尽可能使用input,而不用return

case
result

0x05闭包函数

 如果内层变量可以被外层通过指针方式引用到,那么也会发生逃逸

case
result

0x03 结论

  • 尽可能少用指针
  • 变数尽可能通过传参方式,少用return方式
Past
Now
1
2014/06/12-Start
2
2014/11/29-XXX
3
2015/02/18-DDD
4
More