Contents

GO逃逸分析

栈和堆

  • 分配快,栈分配内存只需要两个CPU指令:“PUSH”和“RELEASE”,随着函数的调用分配和回收
  • 从高位到低位存放函数等数据
  • 查看栈大小
1
2
➜  ~ ulimit -a | grep stack
-s: stack size (kbytes)             8192

  • 分配慢,需要使用new方法申请分配,需要GC回收
  • 从低位到高位存放数据

逃逸分析

概述

  • 是谁进行的:go编译器
  • 为什么而进行:合理化内存分配的位置
  • 如何进行:内存逃逸
  • 逃逸的标准:当前变量是否可以只在声明的作用域中
  • 是好是坏:自我优化的过程,肯定是好的啊(一开始听到逃逸这个词,总感觉不是啥好东西)
  • 有逃逸分析,在编写代码时是否还要注意内存分配:当然需要,应当尽量利用栈分配

查看逃逸分析

使用go build:

1
go build -gcflags="-m -m -l" main.go
  • -m 显示优化决策
  • -l 禁止使用内联

使用反汇编:

1
go tool compile -S main.go

哪些情况发生逃逸

  • 局部变量获取引用
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package main

type S struct {
	M *int
}

func main() {
	var i int
	refStruct(i)
	refStruct1(&i)
}

func refStruct1(y1 *int) (z S) {
	z.M = y1
	return z
}
func refStruct(y int) (z S) {
	z.M = &y
	return z
}
1
2
3
4
5
6
7
8
9
➜  p8  go run -gcflags '-m -m -l' main.go
# command-line-arguments
# ...
./main.go:17:16: y escapes to heap:
./main.go:17:16:   flow: z = &y:
./main.go:17:16:     from &y (address-of) at ./main.go:18:8
./main.go:17:16:     from z.M = &y (assign) at ./main.go:18:6
./main.go:17:16: moved to heap: y
# ...
  • 变量大小不确定及栈空间不足引发逃逸
 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
package main

import (
	"math/rand"
)

func LessThan8192() {
	nums := make([]int, 100) // = 64KB
	for i := 0; i < len(nums); i++ {
		nums[i] = rand.Int()
	}
}

func MoreThan8192() {
	nums := make([]int, 1000000) // = 64KB
	for i := 0; i < len(nums); i++ {
		nums[i] = rand.Int()
	}
}

func NonConstant() {
	number := 10
	s := make([]int, number)
	for i := 0; i < len(s); i++ {
		s[i] = i
	}
}

func main() {
	NonConstant()
	MoreThan8192()
	LessThan8192()
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
➜  p8 go build -gcflags '-m -m -l' main.go
# command-line-arguments
./main.go:8:14: make([]int, 100) does not escape
./main.go:15:14: make([]int, 1000000) escapes to heap:
./main.go:15:14:   flow: {heap} = &{storage for make([]int, 1000000)}:
./main.go:15:14:     from make([]int, 1000000) (too large for stack) at ./main.go:15:14
./main.go:15:14: make([]int, 1000000) escapes to heap
./main.go:23:11: make([]int, number) escapes to heap:
./main.go:23:11:   flow: {heap} = &{storage for make([]int, number)}:
./main.go:23:11:     from make([]int, number) (non-constant size) at ./main.go:23:11
./main.go:23:11: make([]int, number) escapes to heap
  • 指针嵌套导致的逃逸分析失败
 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
package main

type S struct {
	M *int
}

type S1 struct {
	M int
}

func main() {
	var x S
	var i int
	var i1 int
	ref(&i, &x)
	var x1 S1
	ref1(i1, &x1)
	ref2(&i1, x)
}

func ref(y *int, z *S) {
	z.M = y
}

func ref1(y int, z *S1) {
	z.M = y
}
func ref2(y *int, z S) {
	z.M = y
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
➜  p8 go build -gcflags '-m -m -l' main.go
# command-line-arguments
./main.go:21:10: parameter y leaks to {heap} with derefs=0:
./main.go:21:10:   flow: {heap} = y:
./main.go:21:10:     from z.M = y (assign) at ./main.go:22:6
./main.go:21:10: leaking param: y
./main.go:21:18: z does not escape
./main.go:25:18: z does not escape
./main.go:28:11: y does not escape
./main.go:28:19: z does not escape
./main.go:13:6: i escapes to heap:
./main.go:13:6:   flow: {heap} = &i:
./main.go:13:6:     from &i (address-of) at ./main.go:15:6
./main.go:13:6:     from ref(&i, &x) (call parameter) at ./main.go:15:5
./main.go:13:6: moved to heap: i

一些优化

  • 以指针传参,比如下面这个就会引起内存逃逸
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package main

type S struct{}

func main() {
	var x S
	y := x
	_ = *identity(y)
}

func identity(z S) *S {
	return &z
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
➜  p8  go run -gcflags '-m -m -l' main.go
# command-line-arguments
./main.go:11:15: parameter z leaks to ~r0 with derefs=0:
./main.go:11:15:   flow: ~r0 = &z:
./main.go:11:15:     from &z (address-of) at ./main.go:12:9
./main.go:11:15:     from return &z (return) at ./main.go:12:2
./main.go:11:15: z escapes to heap:
./main.go:11:15:   flow: ~r0 = &z:
./main.go:11:15:     from &z (address-of) at ./main.go:12:9
./main.go:11:15:     from return &z (return) at ./main.go:12:2
./main.go:11:15: moved to heap: z

可以优化成:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package main

type S struct{}

func main() {
	var x S
	y := &x
	_ = *identity(y)
}

func identity(z *S) *S {
	return z
}
1
2
3
4
5
6
➜  p8  go run -gcflags '-m -m -l' main.go
# command-line-arguments
./main.go:11:15: parameter z leaks to ~r0 with derefs=0:
./main.go:11:15:   flow: ~r0 = z:
./main.go:11:15:     from return z (return) at ./main.go:12:2
./main.go:11:15: leaking param: z to result ~r0 level=0
  • 无必要不return;下面这种就没必要让编译器进行一次逃逸
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package main

type S struct {}

func main() {
    var x S
    _ = *ref(x)
}

func ref(z S) *S {
    return &z
}

本文参考

coffee