关于golang的逃逸规则

编译时逃逸分析

Go 语言的逃逸分析是在编译期间由编译器处理的。编译器会对代码进行静态分析,以确定哪些变量应该分配在栈上,哪些应该分配在堆上。

逃逸分析的目标是尽可能地减少内存的堆分配,从而提高程序的性能。编译器会尝试确定哪些变量的生命周期受限于其作用域,并且可以安全地分配在栈上。对于那些需要在函数的作用域外部仍然可用的变量,编译器会将它们分配在堆上。

通过逃逸分析,编译器可以做出一些优化决策,例如将对象分配在栈上以提高访问速度,减少堆分配和垃圾回收的开销。这有助于编写更高效的 Go 代码,并减少内存泄漏的可能性。

逃逸分析是 Go 语言编译器的一部分,它在编译过程中自动进行,无需手动干预。编译器会生成逃逸分析的报告,以帮助开发人员了解哪些变量逃逸到堆上,从而可以进一步优化代码。通过查看编译器的逃逸分析报告,开发人员可以识别和解决代码中的性能问题。

逃逸规则

  1. 如果函数外部没有引用,则优先(不是一定)放到栈中;
  2. 如果分配的内存超过了栈的存储能力,会在堆上分配
  3. 如果函数外部存在引用,则必定放到堆中;
  4. 一个引用类对象中的any(interface{})进行赋值,会出现逃逸现象。
  5. 一个引用类对象中的引用类成员进行赋值,会出现逃逸现象。

一般我们给一个引用类对象中的引用类成员进行赋值,可能出现逃逸现象。可以理解为访问一个引用对象实际上底层就是通过一个指针来间接的访问了,但如果再访问里面的引用成员就会有第二次间接访问,这样操作这部分对象的话,极大可能会出现逃逸的现象。Go语言中的引用类型有func(函数类型),interface(接口类型),slice(切片类型),map(字典类型),channel(管道类型),*(指针类型)等。

对于前面3条我们很好理解,我们现在来看第4点。

示例一 map的value是any

package main

func main() {
    data := make(map[int]interface{})
    data[100] = 200
}

//go build -gcflags=-m .\main.go
//main.go:3:6: can inline main
//main.go:4:14: make(map[int]interface {}) does not escape
//main.go:5:14: 200 escapes to heap

结果是200逃逸到堆内存中去了。

示例二 map里的key和value类型都确定

package main

func main() {
    data := make(map[int]int)
    data[100] = 200
}

/*
go build -gcflags=-m .\main.go
# command-line-arguments
./main.go:3:6: can inline main
./main.go:4:14: make(map[int]int) does not escape
*/

结果是没有发生逃逸

示例三 map的key和value都未确定类型

package main

func main() {
    data := make(map[any]any)
    data[100] = 200
}

/*
go build -gcflags=-m .\main.go
# command-line-arguments
./main.go:3:6: can inline main
./main.go:4:14: make(map[any]any) does not escape
./main.go:5:7: 100 escapes to heap
./main.go:5:14: 200 escapes to heap
*/

由于map本身是一个引用类型,而当其键和值转换为interface{}类型,这导致了100和200的逃逸到堆上。编译器无法确定这两个值的具体类型,因此它将它们分配到堆上以确保安全性。在示例三种编译器可以确定key和value的类型都是int,就将其分配到了栈上。

关于第五点我们再来看几个示例:

示例四 map内部使用切片

package main

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

//main.go:3:6: can inline main
//main.go:4:14: make(map[string][]string) does not escape
//main.go:5:24: []string{...} escapes to heap

示例五 切片

package main

func main() {
    data := []string{"value"}
    data = append(data, "")
}

/*
./main.go:3:6: can inline main
./main.go:4:18: []string{...} does not escape
*/

如果data本身是个map(引用类型),在map里面的value又是一个slice(切片类型)也是引用类型,那这个切片就会溢出。

示例六 切片内部使用指针

package main

func main() {
    a := 10
    data := []*int{nil}
    data[0] = &a
}

/*
./main.go:3:6: can inline main
./main.go:4:2: moved to heap: a
./main.go:5:16: []*int{...} does not escape
*/

如果data本身是个切片(引用类型),在data里面的value又是一个int也是引用类型,那这个int类型的a就会溢出。

示例七 通道内部使用引用类型

package main

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

    s := []string{"aceld"}

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

/*
./main.go:8:5: can inline main.func1
./main.go:6:15: []string{...} escapes to heap
./main.go:8:5: func literal escapes to heap
*/

示例八 函数参数使用引用类型

package main

import "fmt"

func foo(a *int) {
    return
}

func main() {
    data := 10
    f := foo
    f(&data)
    fmt.Println(data)
}
/*
./main.go:5:6: can inline foo
./main.go:12:3: inlining call to foo
./main.go:13:13: inlining call to fmt.Println
./main.go:5:10: a does not escape
./main.go:13:13: ... argument does not escape
./main.go:13:14: data escapes to heap
*/

你可能感兴趣的:(go)