Go 逃逸分析

简介

Go 程序会在两个地方为变量分配内存。一个是全局空间用来动态分配内存。另一个是每个Goroutine的空间。Go语言实现了垃圾回收机制,虽然不用担心内存是分配到栈空间,还是堆空间,但不同的分配方式,存在着不同的性能差异。

  • 在函数中申请一个对象,如果分配到栈中,函数执行结束后自动回收。如果分配到堆中,则在函数结束后某个时间点进行垃圾回收
  • 在栈上分配和回收内存的开销很低。在堆上分配内存,一个很大的额外开销则是垃圾回收。Go 语言使用的是标记清除算法,并且在此基础上使用了三色标记法和写屏障技术,提高了效率。
  • Go 编译器确定某个变量是分配在栈上还是堆上的过程,被称为逃逸分析。逃逸分析由编译器完成,作用于编译阶段

逃逸出现情况

  1. 指针逃逸

    当函数中创建了一个对象,并返回了这个对象的地址。当函数退出时,因为指针的存在,对象的内存不能随着函数结束而回收,因此对象只能逃逸分配到堆中。

    package main
    
    import "fmt"
    
    func getPointer() *struct{} {
        a := new(struct{})
        return a
    }
    
    func main() {
        p := getPointer()
        fmt.Println(p)
    }
    -- 通过选项 -gcflags=-m,查看变量逃逸的情况
    -- go build -gcflags=-m test.go
    
    $ go build -gcflags=-m test.go
    # command-line-arguments
    .\test.go:5:6: can inline getPointer
    .\test.go:11:17: inlining call to getPointer   
    .\test.go:12:13: inlining call to fmt.Println  
    .\test.go:6:10: new(struct {}) escapes to heap 
    .\test.go:11:17: new(struct {}) escapes to heap
    .\test.go:12:13: ... argument does not escape  
  1. 闭包

    当函数内部的局部变量被匿名函数捕获并在函数外被调用时,这些局部变量就会逃逸到堆上

    package main
    
    import "fmt"
    
    func closureEscape() func() int {
        x := 10
        // 返回一个闭包,该闭包捕获了外部函数的局部变量x
        return func() int {
            x += 5
            return x
        }
    }
    
    func main() {
        // 调用closureEscape函数,并将返回的闭包赋值给变量f
        f := closureEscape()
    
        // 在main函数外部,继续使用闭包f,这时闭包中的局部变量x就逃逸到堆上
        fmt.Println(f()) // 输出:15
        fmt.Println(f()) // 输出:20
    }
    
    -- 通过选项 -gcflags=-m,查看变量逃逸的情况
    -- go build -gcflags=-m test.go
    
    $ go build -gcflags=-m test.go
    # command-line-arguments
    .\test.go:5:6: can inline closureEscape        
    .\test.go:8:9: can inline closureEscape.func1  
    .\test.go:16:20: inlining call to closureEscape
    .\test.go:8:9: can inline main.func1
    .\test.go:19:15: inlining call to main.func1   
    .\test.go:19:13: inlining call to fmt.Println  
    .\test.go:20:15: inlining call to main.func1   
    .\test.go:20:13: inlining call to fmt.Println  
    .\test.go:6:2: moved to heap: x
    .\test.go:8:9: func literal escapes to heap    
    .\test.go:16:20: func literal does not escape  
    .\test.go:19:13: ... argument does not escape  
    .\test.go:19:15: ~R0 escapes to heap
    .\test.go:20:13: ... argument does not escape  
    .\test.go:20:15: ~R0 escapes to heap
  1. 变量占用内存较大 / 大小不确定

    由于栈所能分配的空间有限,当分配的内存超过了Go 编译器的阈值时,所分配的内存就会逃逸到堆上。

    同时,如果分配的大小不确定,编译器不确定后续是否是大内存,此时也将变量在堆上分配。

    package main
    
    import "math/rand"
    
    func genLowM() {
        nums := make([]int, 10)
        for i := 0; i < 10; i++ &#123;
            nums[i] = rand.Int()
        &#125;
    &#125;
    
    func genHigtM() &#123;
        nums := make([]int, 10000)
        for i := 0; i < 10000; i++ &#123;
            nums[i] = rand.Int()
        &#125;
    &#125;
    
    func generate(n int) &#123;
        nums := make([]int, n) // 不确定大小
        for i := 0; i < n; i++ &#123;
            nums[i] = rand.Int()
        &#125;
    &#125;
    
    func main() &#123;
        genLowM()
        genHigtM()
        generate(1)
    &#125;
    
    $ go build -gcflags=-m test.go
    # command-line-arguments
    .\test.go:6:14: make([]int, 10) does not escape    
    .\test.go:13:14: make([]int, 10000) escapes to heap
    .\test.go:20:14: make([]int, n) escapes to heap    
  1. 动态类型(interface{})逃逸

    如果变量是interface{}类型,编译期无法确定其具体的参数类型,所以内存分配到堆中。

    package main
    
    import (
        "fmt"
    )
    
    func main() &#123;
        a := "1"
        fmt.Println(a)
    &#125;
    
    // fmt.Println() 的参数是interface&#123;&#125;类型
    
    $ go build -gcflags=-m test.go
    # command-line-arguments
    .\test.go:9:13: inlining call to fmt.Println
    .\test.go:9:13: ... argument does not escape
    .\test.go:9:13: a escapes to heap

如何利用逃逸分析提高性能

​ 在函数参数传递时,传值会拷贝整个对象,而传指针只会拷贝指针地址,指向的对象是同一个。传指针可以减少值的拷贝,但是会导致内存分配逃逸到堆上,增加垃圾回收(GC)的负担。如果当对象频繁创建和删除时,传递指针会导致GC的开销影响性能。

对于需要修改原对象值,或占用较大内存的对象,需要选择传递指针。

对于只读且占用内存较小的对象,直接传值可以获得更好的性能