深入学习Go-9 逃逸分析

逃逸分析发生在编译阶段,是指由编译器决定变量被分配在栈区还是堆区。如果分配在栈区,当函数执行完成后内存自动释放;如果分配在堆区,函数执行完成后由垃圾回收机制释放内存。

不管是字符串、数组,还是通过new、make标识符创建的对象都由逃逸分析来决定是分配在栈区还是堆区。

逃逸策略

  • 变量的使用范围:如果函数中的变量没有被函数外部引用,则优先分配到栈中;如果函数中的变量被函数外部引用,则分配到堆中;

  • 变量占用的内存大小:如果栈空间不足,则变量分配到堆中。

  • 变量类型是否确定:编译期不能确定变量的具体类型,则分配到堆中。

  • 变量长度是否确定:编译期不能确定长度的变量,则分配到堆中。

场景分析

通过编译器命令go build -gcflags可以查看程序是否有逃逸现象发生:

go build -gcflags '-m -l' xxx.go

// 查看详细信息
go build -gcflags '-m -m -l' xxx.go

下面我们通过例子来分析逃逸策略的场景,通过以上命令来查看逃逸结果。

变量的使用范围

变量的使用范围包括指针逃逸、引用类型逃逸和闭包引用变量逃逸。

指针逃逸

func ptr() *int {
    i := 1
    return &i
}

func ptrEscape() {
    ptr()
}

逃逸分析:

go build -gcflags '-m -m -l' main.go escape.go
# command-line-arguments
./escape.go:4:2: i escapes to heap:
./escape.go:4:2:   flow: ~r0 = &i:
./escape.go:4:2:     from &i (address-of) at ./escape.go:5:9
./escape.go:4:2:     from return &i (return) at ./escape.go:5:2
./escape.go:4:2: moved to heap: i

从结果可以看出变量i已经逃逸到堆中。

引用类型逃逸

func ref() []int {
    ai := []int{1, 2, 3, 4, 5}
    return ai
}

func refEscape() {
    ref()
}

逃逸分析:

go build -gcflags '-m -m -l' main.go escape.go
# command-line-arguments
./escape.go:13:13: []int{...} escapes to heap:
./escape.go:13:13:   flow: ai = &{storage for []int{...}}:
./escape.go:13:13:     from []int{...} (spill) at ./escape.go:13:13
./escape.go:13:13:     from ai := []int{...} (assign) at ./escape.go:13:5
./escape.go:13:13:   flow: ~r0 = ai:
./escape.go:13:13:     from return ai (return) at ./escape.go:14:2
./escape.go:13:13: []int{...} escapes to heap

由于切片结构体中包含指向底层数组的指针,因此切片[]i也逃逸到了堆中。

闭包引用变量逃逸

func closure() func() int {
    i := 0
    return func() int {
        i++
        return i
    }
}

func closureEscape() {
    closure()
}

逃逸分析:

go build -gcflags '-m -m -l' main.go escape.go
# command-line-arguments
./escape.go:24:3: closure.func1 capturing by ref: i (addr=true assign=true width=8)
./escape.go:23:9: func literal escapes to heap:
./escape.go:23:9:   flow: ~r0 = &{storage for func literal}:
./escape.go:23:9:     from func literal (spill) at ./escape.go:23:9
./escape.go:23:9:     from return func literal (return) at ./escape.go:23:2
./escape.go:22:2: i escapes to heap:
./escape.go:22:2:   flow: {storage for func literal} = &i:
./escape.go:22:2:     from func literal (captured by a closure) at ./escape.go:23:9
./escape.go:22:2:     from i (reference) at ./escape.go:24:3
./escape.go:22:2: moved to heap: i
./escape.go:23:9: func literal escapes to heap

变量占用的内存大小

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

逃逸分析:

go build -gcflags '-m -m -l' main.go escape.go
# command-line-arguments
./escape.go:34:11: make([]int, 1000) does not escape

当切片的长度为1000时没有发生逃逸,我们把长度放大10倍看看:

func memOccupy() {
    s := make([]int, 10000)
    for i := range s {
        s[i] = i
    }
}

逃逸分析:

go build -gcflags '-m -m -l' main.go escape.go
# command-line-arguments
./escape.go:34:11: make([]int, 10000) escapes to heap:
./escape.go:34:11:   flow: {heap} = &{storage for make([]int, 10000)}:
./escape.go:34:11:     from make([]int, 10000) (too large for stack) at ./escape.go:34:11
./escape.go:34:11: make([]int, 10000) escapes to heap

从分析结果“(too large for stack)”看出,切片需要分配的内存太大,栈空间不足,因此逃逸到了堆中。

变量类型不确定

func typeEscape() {
    i := 0
    fmt.Println(i)
}

逃逸分析:

go build -gcflags '-m -m -l' main.go escape.go
# command-line-arguments
./escape.go:44:13: i escapes to heap:
./escape.go:44:13:   flow: {storage for ... argument} = &{storage for i}:
./escape.go:44:13:     from i (spill) at ./escape.go:44:13
./escape.go:44:13:     from ... argument (slice-literal-element) at ./escape.go:44:13
./escape.go:44:13:   flow: {heap} = {storage for ... argument}:
./escape.go:44:13:     from ... argument (spill) at ./escape.go:44:13
./escape.go:44:13:     from fmt.Println(... argument...) (call parameter) at ./escape.go:44:13
./escape.go:44:13: ... argument does not escape
./escape.go:44:13: i escapes to heap

由于fmt.Println()的参数类型是interface{},在编译器不能确定其参数类型,因此逃逸到堆中。

变量长度不确定

func varLenEscape() {
    len := 10
    s := make([]int, 1, len)
    s[0] = 1
}

逃逸分析:

go build -gcflags '-m -m -l' main.go escape.go
# command-line-arguments
./escape.go:47:11: make([]int, 1, len) escapes to heap:
./escape.go:47:11:   flow: {heap} = &{storage for make([]int, 1, len)}:
./escape.go:47:11:     from make([]int, 1, len) (non-constant size) at ./escape.go:47:11
./escape.go:47:11: make([]int, 1, len) escapes to heap

输出结果“ (non-constant size)”,变量长度不确定逃逸到堆中。

总结

逃逸分析发生在编译阶段,由编译器决定变量分配在栈区还是堆区。逃逸策略分为四种情况:根据变量的使用范围进行分析;根据变量占用内存的大小进行分析;根据变量的类型是否确定进行分析;根据变量的大小是否确定进行分析。


更多【分布式专辑】【架构实战专辑】系列文章,请关注公众号:coding到灯火阑珊

图片

猜你喜欢

转载自blog.csdn.net/lonewolf79218/article/details/122100264