当前位置:网站首页>gol内存逃逸

gol内存逃逸

2021-11-25 18:13:36 快乐的真谛

总结

  • 逃逸的指针变量本身也是分配在堆空间,故函数可以返回局部变量的地址。此时的局部变量相当于局部指针变量,逃逸时,指针变量本身也是分配在堆空间,所以能返回它的地址。
  • 栈空间的内存由编译器管理,分配释放速度很快。堆空间,由gc管理,频繁的gc会占用系统较大的开销,stop the world
  • 逃逸分析是编译器在静态编译时完成的。
  • 切片变量本身逃逸了,那它底层的data区域也会逃逸。即使切片长度很小。
  • 切片变量本身没逃逸,那一般情况它的data区域也在栈上,若长度太长,则data区域会分配到堆上,但切片变量本身还是在栈上。

如何确定内存逃逸?

go run -gcflags '-m -l' main.go

注意:上述命令只在编译器接入判断是否逃逸处有输出。
有时候栈空间比堆空间地址还小,不知道为啥。

内存逃逸的例子

func main()  {
    a := 1
    _ = a
}

// 无输出
func main()  {
    a := 1
    fmt.Printf("%p\n", &a)
    fmt.Println(a)

    b := 2
    fmt.Printf("%p\n", &b)
    fmt.Println(b)

    c := 3
    fmt.Printf("%p\n", &c)
    fmt.Println(c)

    d := 4
    fmt.Printf("%p\n", &d)
    fmt.Println(d)
}

// 输出
./main.go:10:2: moved to heap: a
./main.go:14:2: moved to heap: b
./main.go:18:2: moved to heap: c
./main.go:22:2: moved to heap: d
./main.go:11:12: ... argument does not escape
./main.go:12:13: ... argument does not escape
./main.go:12:13: a escapes to heap
./main.go:15:12: ... argument does not escape
./main.go:16:13: ... argument does not escape
./main.go:16:13: b escapes to heap
./main.go:19:12: ... argument does not escape
./main.go:20:13: ... argument does not escape
./main.go:20:13: c escapes to heap
./main.go:23:12: ... argument does not escape
./main.go:24:13: ... argument does not escape
./main.go:24:13: d escapes to heap
0xc000012080
1
0xc000012088
2
0xc0000120a0
3
0xc0000120a8
4

fmt.Printf函数abc赋值给interface{},造成逃逸,abc地址在堆空间,如图正增长,印证了在堆上。对比下面:

func main()  {
    a := 1
    println(&a)
    println(a)

    b := 2
    println(&b)
    println(b)

    c := 3
    println(&c)
    println(c)

    d := 4
    println(&d)
    println(d)
}

// 输出
0xc00002e768
1
0xc00002e760
2
0xc00002e758
3
0xc00002e750
4

println不会触发内存逃逸,abc分配在栈空间,地址从高到低增长印证了这点。

四个指针没逃逸的情况:

func main()  {
    a := new(int)
    println(&a)
    println(a)

    println("======")
    b := new(int)
    println(&b)
    println(b)

    println("======")
    c := new(int)
    println(&c)
    println(c)

    println("======")
    d := new(int)
    println(&d)
    println(d)
}

// 输出
./main.go:32:10: new(int) does not escape
./main.go:39:10: new(int) does not escape
./main.go:46:10: new(int) does not escape
./main.go:53:10: new(int) does not escape
0xc00002e768
0xc00002e740
======
0xc00002e760
0xc00002e738
======
0xc00002e758
0xc00002e730
======
0xc00002e750
0xc00002e748

abcd指针变量本身和其所指的内存区域,没有逃逸,都是分配在栈空间,打印的地址从大到小为连续区域印证了这点。
内存分配如图:
image.png

四个指针有逃逸的情况

func main() {
    a := new(int)
    fmt.Printf("%p\n", &a)
    fmt.Println(a)
    println("======")

    b := new(int)
    fmt.Printf("%p\n", &b)
    fmt.Println(b)

    println("======")
    c := new(int)
    fmt.Printf("%p\n", &c)
    fmt.Println(c)
    println("======")

    d := new(int)
    fmt.Printf("%p\n", &d)
    fmt.Println(d)

    println("======")
    f := new(int)
    println(&f)
    println(f)
}

//输出
0xc00000e028
0xc000012080
======
0xc00000e038
0xc000012088
======
0xc00000e040
0xc0000120a0
======
0xc00000e048
0xc0000120a8
======
0xc000058f38
0xc000058f30

逃逸情况下,abcd指针变量本身和其所指的内存区域,都是分配在堆空间,打印的地址从小到大为连续区域印证了这点。f指针变量和其所指的内存区域分配在栈空间,其地址高于堆区域地址印证了这点。
逃逸的指针变量本身也是分配在堆空间,故函数可以返回局部指针变量
内存分配图如下:
image.png


func main()  {
    a := new(int)
    _ = a
}

// 输出
./main.go:8:10: main new(int) does not escape

即使是new的变量,没逃逸时,也是分配在栈

  • println函数不会触发逃逸,而fmt.Printf会,因为它的函数参数是interface{}类型
  • 注意: 上面例子,a是指针变量分配在栈空间,指向的int存储单元在下一个字节(也是在栈空间)。
  • 栈空间在高地址为,从高往低分配。

这要是在C++中这么写,是个很典型的错误:返回局部变量的地址,该地址的内容在函数退出后会被自动释放,因为是在栈上的。

那么go语言的局部变量到底是在栈上还是堆上呢?go语言编译器会做逃逸分析(escape analysis),分析局部变量的作用域是否逃出函数的作用域,要是没有,那么就放在栈上;要是变量的作用域超出了函数的作用域,那么就自动放在堆上。

测试观察g的地址变化

func main {
    var g *int
    println(&g)
    println(g)
    g = new(int)
    println(&g)
    println(g)
    g = new(int)
    println(&g)
    println(g)
    g = new(int)
    fmt.Println(&g)
    fmt.Println(g)
}

没有最后两行的话,没逃逸,g与g指向的内存都在栈区,加了则都在堆区。

出现逃逸的场景

package main

type Student struct {
 Name interface{}
}

func main()  {
 stu := new(Student)
 stu.Name = "tom"

}

interface{} 赋值,会发生逃逸,优化方案是将类型设置为固定类型,例如:string

package main

type Student struct {
 Name string
}

func GetStudent() *Student {
 stu := new(Student)
 stu.Name = "tom"
 return stu
}

func main() {
 GetStudent()
}

返回(局部变量的地址)指针类型,会发生逃逸,优化方案视情况而定。
函数传递指针和传值哪个效率高吗?我们知道传递指针可以减少底层值的拷贝,可以提高效率,但是如果拷贝的数据量小,由于指针传递会产生逃逸,可能会使用堆,也可能会增加 GC 的负担,所以传递指针不一定是高效的。
不要盲目使用变量指针作为参数,虽然减少了复制,但变量逃逸的开销可能更大。

func main()  {
    nums := make([]int, 5, 5)
    nums2 := make([]int, 5, 5)
    println(&nums)
    println(nums)
    println(&nums2)
    println(nums2)
    for i := range nums {
        nums[i] = i
        println(&nums[i])
    }
    println("======2")
    for i := range nums2 {
        nums2[i] = i
        println(&nums2[i])
    }

    println("======3")
    nums3 := make([]int, 10000, 10000)
    println(&nums3)
    println(nums3)
    for i := range nums3 {
        if i == 5 {
            break
        }
        nums3[i] = i
        println(&nums3[i])
    }
    //fmt.Println(&nums3)

}

// 输出
./main.go:8:14: make([]int, 5, 5) does not escape
./main.go:9:15: make([]int, 5, 5) does not escape
./main.go:25:15: make([]int, 10000, 10000) escapes to heap
0xc00002e758
[5/5]0xc00002e6f8
0xc00002e740
[5/5]0xc00002e6d0
0xc00002e6f8
0xc00002e700
0xc00002e708
0xc00002e710
0xc00002e718
======2
0xc00002e6d0
0xc00002e6d8
0xc00002e6e0
0xc00002e6e8
0xc00002e6f0
======3
0xc00002e728
[10000/10000]0xc000054000
0xc000054000
0xc000054008
0xc000054010
0xc000054018
0xc000054020

如上:第1、2个slice变量及其底层的data指针区域,都是分配在栈上。
第3个超大切片,slice变量本身分配在栈上,底层数据在堆上。
如果注释掉最后的fmt.Printf则第3个slice变量本身也会逃逸分配在堆上。

注意:切片data区域第1到第n个元素的地址空间都是从0往上增的,不管是在栈还是在堆上。

func main()  {
    var slice1 []int
    println(&slice1)
    println(slice1)
    fmt.Printf("%p\n", &slice1)
    println(&slice1)
    println(slice1)
    slice1 = make([]int,5,5)
    println(&slice1)
    println(slice1)
}

// 输出
./main.go:11:6: moved to heap: slice1
./main.go:14:12: ... argument does not escape
./main.go:17:15: make([]int, 5, 5) escapes to heap
0xc0000a4018
[0/0]0x0
0xc0000a4018
0xc0000a4018
[0/0]0x0
0xc0000a4018
[5/5]0xc0000aa030

切片变量本身逃逸了,那它底层的data区域也会逃逸。即使切片长度很小。
切片变量本身没逃逸,那一般情况它的data区域也在栈上,若长度太长,则data区域会分配到堆上,但切片变量本身还是在栈上。

package main

func main() {
 nums := make([]int, 10000, 10000)

 for i := range nums {
  nums[i] = i
 }
}

切片太大,会发生逃逸,优化方案尽量设置容量,如果容量实在过大那就没办法了。

map里的元素是不能取地址的。

map没逃逸时,也是分配在栈上的,变量本身及底层的数据区。

版权声明
本文为[快乐的真谛]所创,转载请带上原文链接,感谢
https://segmentfault.com/a/1190000041018336

随机推荐