Go内存逃逸分析

基本概念

逃逸分析(Escape analysis)是由编译器决定内存分配位置的一种技术,不需要程序员明确指定。函数中申请一个新对象时,编译器决定该对象是分配在栈中还是堆中:

  • 如果分配在栈中,函数执行结束后内存会自动回收。
  • 如果分配在堆中,函数执行结束后内存会交给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逃逸到堆
    s.Name = name
    s.Age = age
    return s
}

func main() {
    StudentRegister("Jim", 18)
}

通过编译参数 -gcflag=-m 可以查看编译过程中的逃逸分析:

# _/D_/SourceCode/GoExpert/src
.\main.go:8: new(Student) escapes to heap

可见在 StudentRegister() 函数中,代码第9行显示“escapes to heap”,表示该行内存分配发生了逃逸现象。

2、栈空间不足逃逸

下面的代码 Slice() 函数中分配了一个1000个长度的切片,是否逃逸取决于栈空间是否足够大。

package main

func Slice() {
    s := make([]int, 1000, 1000)
    for index := range s {
        s[index] = index
    }
}

func main() {
    Slice()
}

查看编译提示:

# _/D_/SourceCode/GoExpert/src
.\main.go:4: Slice make([]int, 1000, 1000) does not escape

当切片长度扩大到10000时,编译提示如下:

# _/D_/SourceCode/GoExpert/src
.\main.go:4: make([]int, 10000, 10000) escapes to heap

当栈空间不足以存放当前对象时或无法判断当前切片长度时,会将对象分配到堆中。

3、动态类型逃逸

很多函数参数为 interface 类型,比如 fmt.Println(a ...interface{}),编译期间很难确定其参数的具体类型,也会产生逃逸。如下代码所示:

package main

import "fmt"

func main() {
    s := "Escape"
    fmt.Println(s)
}

查看编译提示:

# _/D_/SourceCode/GoExpert/src
.\main.go:7: s escapes to heap

4、闭包引用对象逃逸

某著名的开源框架实现了一个返回Fibonacci数列的函数。该函数返回一个闭包,闭包引用了函数的局部变量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() 函数中原本属于局部变量的 a 和 b 由于闭包的引用,不得不将二者放到堆上,以致产生逃逸。

逃逸分析的目的

  • 栈上分配内存比在堆中分配内存有更高的效率。
  • 栈上分配的内存不需要GC处理。
  • 堆上分配的内存使用完毕会交给GC处理。
  • 逃逸分析目的是决定内存分配地址是栈还是堆,逃逸分析在编译阶段完成。

通过了解逃逸分析,可以更好地优化代码并提高程序性能。

打 赏