Go内存逃逸

Go内存逃逸

Go内存逃逸

简单来说就是原本应在栈上分配内存的对象,逃逸到了堆上进行分配。如果能在栈上进行分配,那么只需要两个指令,入栈和出栈,GC压力也小了。所以相比之下,在栈上分配代价会小很多。

go语言编译器会自动决定把一个变量放在栈还是放在堆,编译器会做逃逸分析(escape analysis)当发现变量的作用域没有跑出函数范围,就可以在栈上,反之则必须分配在堆

指针逃逸

package main

func foo(arg_val int)(*int) {

    var foo_val int = 11;
    return &foo_val;
}

func main() {

    main_val := foo(666)

    println(*main_val)
}
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)
}

栈空间不足逃逸(空间开辟过大)

package main

func Slice() {
    s := make([]int,  10000, 10000)

    for index, _ := range s {
        s[index] = index
    }
}

func main() {
    Slice()
}

动态类型逃逸(不确定长度大小)

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

又或者像前面提到的例子:

func F() {
    a := make([]int, 0, 20)     // 栈 空间小
    b := make([]int, 0, 20000) // 堆 空间过大 逃逸

    l := 20
    c := make([]int, 0, l) // 堆 动态分配不定空间 逃逸
}
func main() {
    data := []interface{}{100, 200}
    data[0] = 100
}
//结果
aceld:test ldb$ go tool compile -m 1.go

1.go:3:6: can inline main
1.go:4:23: []interface {}{...} does not escape
1.go:4:24: 100 does not escape
1.go:4:29: 200 does not escape
1.go:6:10: 100 escapes to heap
func main() {
    data := make(map[string]interface{})
    data["key"] = 200
}

aceld:test ldb$ go tool compile -m 2.go
2.go:3:6: can inline main
2.go:4:14: make(map[string]interface {}) does not escape
2.go:6:14: 200 escapes to heap
func main() {
    data := make(map[interface{}]interface{})
    data[100] = 200
}
//
aceld:test ldb$ go tool compile -m 3.go
3.go:3:6: can inline main
3.go:4:14: make(map[interface {}]interface {}) does not escape
3.go:6:6: 100 escapes to heap
3.go:6:12: 200 escapes to heap

闭包引用对象逃逸

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())
    }
}

引用对象逃逸

package main

func main() {
    data := make(map[string][]string)
    data["key"] = []string{"value"}
}

我们通过编译看看逃逸结果

aceld:test ldb$ go tool compile -m 4.go
4.go:3:6: can inline main
4.go:4:14: make(map[string][]string) does not escape
4.go:6:24: []string{...} escapes to heap
//因为map的值为[]string切片,由于切片为引用值类型,所以一开始编译不确定
package main
func main() {
    a := 10
    data := []*int{nil}
    data[0] = &a
}

我们通过编译看看逃逸结果

 aceld:test ldb$ go tool compile -m 5.go
5.go:3:6: can inline main
5.go:4:2: moved to heap: a
5.go:6:16: []*int{...} does not escape
//因为data存的是引用类型值,所以a发生逃逸
package main

func main() {
    ch := make(chan []string)

    s := []string{"aceld"}

    go func() {
        ch <- s
    }()
}

我们通过编译看看逃逸结果

aceld:test ldb$ go tool compile -m 8.go
8.go:8:5: can inline main.func1
8.go:6:15: []string{...} escapes to heap
8.go:8:5: func literal escapes to heap

我们看到[]string{...} escapes to heap, s被逃逸到堆上。

形参为引用类型

func(*int)函数类型,进行函数赋值,会使传递的形参出现逃逸现象,形参为引用类型。

package main

import "fmt"

func foo(a *int) {
    return
}

func main() {
    data := 10
    f := foo
    f(&data)
    fmt.Println(data)
}

我们通过编译看看逃逸结果

aceld:test ldb$ go tool compile -m 6.go
6.go:5:6: can inline foo
6.go:12:3: inlining call to foo
6.go:14:13: inlining call to fmt.Println
6.go:5:10: a does not escape
6.go:14:13: data escapes to heap
6.go:14:13: []interface {}{...} does not escape
:1: .this does not escape

我们会看到data已经被逃逸到堆上。

  • func([]string): 函数类型,进行[]string{"value"}赋值,会使传递的参数出现逃逸现象。
package main

import "fmt"

func foo(a []string) {
    return
}

func main() {
    s := []string{"aceld"}
    foo(s)
    fmt.Println(s)
}

我们通过编译看看逃逸结果

aceld:test ldb$ go tool compile -m 7.go
7.go:5:6: can inline foo
7.go:11:5: inlining call to foo
7.go:13:13: inlining call to fmt.Println
7.go:5:10: a does not escape
7.go:10:15: []string{...} escapes to heap
7.go:13:13: s escapes to heap
7.go:13:13: []interface {}{...} does not escape
 :1: .this does not escape

我们看到 s escapes to heap,s被逃逸到堆上。

逃逸总结

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

   转载规则


《Go内存逃逸》 朱林刚 采用 知识共享署名 4.0 国际许可协议 进行许可。
 上一篇
Go函数 Go函数
Go函数Go函数定义func name(params)(return params){ function body } //eg,如果相邻的参数类型是相同的,则可以省略前一个类型 func cal(a,b int) int{ retur
2021-11-18
下一篇 
Go内存管理与分配 Go内存管理与分配
Go内存管理与分配Go内存管理Go内存会分成堆区(Heap)和栈区(Stack)两个部分,程序在运行期间可以主动从堆区申请内存空间,这些内存由内存分配器分配并由垃圾收集器负责回收。栈区的内存由编译器自动进行分配和释放,栈区中存储着函数的参数
2021-11-18
  目录