Go逃逸分析

2022-09-06 11:06:45 浏览数 (2)

一个对象本应该分配在栈上,结果分配在了堆上,这就是内存逃逸。

内存逃逸的场景

  • 局部指针返回
  • 栈空间不足
  • 动态类型
  • 闭包引用

分析内存逃逸的命令:go build -gcflags='-m -l' memory_analysis.go,'-l'参数可以禁止内联。

若出现 xxx escapes to heap,则xxx变量是发生了内存逃逸,需要尽量避免内存逃逸,因为栈内存的回收效率比堆内存高很多。

场景

局部指针返回

方法内部定义了一个局部指针,并且将这个指针作为返回值返回时,此时就发生了内存逃逸。

逃逸代码片段举例如下:

代码语言:go复制
type student struct {
    name string
}

func returnPointer() *student {
   stu := &student{
       name: "Mike",
   }
   return stu
}

func main() {
    returnPointer()
}

returnPointer函数返回了student对象的指针,会导致该对象逃逸到堆上。

逃逸分析输出:

代码语言:txt复制
./memory_analysis.go:8:12: &student{...} escapes to heap

&student{...} escapes to heap表示对象已经逃逸到堆上。

栈空间不足

整个系统的栈空间本身就不大,本测试环境的栈空间为8M,Goroutine的占用空间不到10KB,分配给栈的空间更小,一旦某个对象的占用空间过大,此时就发生了内存逃逸。当栈内单个对象大小超过64KB,则会发生内存逃逸,channel空间不足也会发生逃逸。

ulimit -a 输出栈空间stack size信息(8192K)如下:

代码语言:txt复制
core file size          (blocks, -c) 0
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 63068
max locked memory       (kbytes, -l) 64
max memory size         (kbytes, -m) unlimited
open files                      (-n) 65535
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 8192
cpu time               (seconds, -t) unlimited
max user processes              (-u) 63068
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited

逃逸代码片段举例如下:

代码语言:go复制
func stackSpace() {
    space1 := make([]int, 100, 100)
    for i := 0; i < len(space1); i   {
        space1[i] = i
    }

    space2 := make([]int, 1000, 1000)
    for i := 0; i < len(space2); i   {
        space2[i] = i
    }
}

func main() {
    stackSpace()
}

stackSpace函数分别make了大小为100和10000的两个整型数组,后者可能会因为栈空间不足发生逃逸。

逃逸分析输出:

代码语言:txt复制
./memory_analysis.go:15:19: make([]int, 100, 100) does not escape
./memory_analysis.go:20:19: make([]int, 10000, 10000) escapes to heap

make([]int, 100, 100) does not escape表示大小为100的space1数组没有发生逃逸。

make([]int, 10000, 10000) escapes to heap表示大小为10000的space2数组发生了逃逸。

由于切片一般都是使用在函数传递的场景下,而且切片在 append 的时候可能会涉及到重新分配内存,如果切片在编译期间的大小不能够确认或者大小超出栈的限制,多数情况下都会分配到堆上。

动态分配

当动态分配内存大小,也会发生逃逸,如动态分配数组大小。

逃逸代码片段举例如下:

代码语言:go复制
func stackSpace() {
    length := 10
    space3 := make([]int, length)
    for i := 0; i < len(space3); i   {
        space3[i] = i
    }
}

逃逸分析输出:

代码语言:shell复制
./memory_analysis.go:26:19: make([]int, length) escapes to heap

make([]int, length) escapes to heap表示已经逃逸到堆上了,不管初始化的数组大小是多大。

动态类型

当被调用函数的入参是interface或者是不定参数,此时就发生了内存逃逸。

逃逸代码片段举例如下:

代码语言:go复制
func main() {
    val := "Mike"
    fmt.Println("Hello world.", val)
}

fmt.Println函数本身接收的参数是不定参数,Println源码如下,会导导致内存逃逸。

代码语言:go复制
// Println formats using the default formats for its operands and writes to standard output.
// Spaces are always added between operands and a newline is appended.
// It returns the number of bytes written and any write error encountered.
func Println(a ...interface{}) (n int, err error) {
	return Fprintln(os.Stdout, a...)
}

逃逸分析输出:

代码语言:shell复制
./memory_analysis.go:69:16: ... argument does not escape
./memory_analysis.go:69:17: "Hello world." escapes to heap
./memory_analysis.go:69:17: val escapes to heap

"Hello world." escapes to heapval escapes to heap表示这两个变量都已经发生了逃逸。

闭包引用

当函数内含有闭包函数时,此时也会发生内存逃逸。

逃逸代码片段举例如下:

代码语言:go复制
func closure() func() string {
    name := "Mike"
    return func() string {
        return "Hello World."   name
    }
}

func main() {
    fmt.Println(closure())
}

closure函数返回了一个闭包函数,此时会发生逃逸。

逃逸分析输出:

代码语言:shell复制
./memory_analysis.go:56:12: func literal escapes to heap
./memory_analysis.go:57:31: "Hello World."   name escapes to heap

func literal escapes to heap表示函数已经发生了逃逸。

局部变量name因为匿名函数返回出去后,编译器认为应该分配在堆上,也发生了逃逸。

取消逃逸分析

编译器默认会进行逃逸分析,会通过规则判定一个变量是分配到堆上还是栈上。一些函数虽然逃逸分析将其存放到堆上。但是对于我们来说需要放在栈上时,我们就可以使用 //go:noescape 指令强制要求编译器将其分配到函数栈上。比如Go官方的memmove函数使用了避免逃逸分析指令。

代码语言:go复制
//go:noescape
func memmove(to, from unsafe.Pointer, n uintptr)

该指令适用于一个有声明但没有主体的函数,函数的实现可能是其它语言类型,不允许编译器对其做逃逸分析。

类似的函数还有以下这些:

代码语言:go复制
//go:noescape
func mapaccess(t *rtype, m unsafe.Pointer, key unsafe.Pointer) (val unsafe.Pointer)

//go:noescape
func mapaccess_faststr(t *rtype, m unsafe.Pointer, key string) (val unsafe.Pointer)

//go:noescape
func mapassign(t *rtype, m unsafe.Pointer, key, val unsafe.Pointer)

//go:noescape
func mapassign_faststr(t *rtype, m unsafe.Pointer, key string, val unsafe.Pointer)

//go:noescape
func mapdelete(t *rtype, m unsafe.Pointer, key unsafe.Pointer)

若直接对普通的Go函数加上此指令,则会出现如下错误

代码语言:txt复制
./memory_analysis.go:8:6: can only use //go:noescape with external func implementations

0 人点赞