go 栈内存和堆内存概念以及内存逃逸分析

为了让程序员更好地专注于业务代码的实现,Go 语言增加了垃圾回收机制,自动地回收不再使用的内存。Go 语言有两部分内存空间:栈内存和堆内存。

1. 栈内存
栈只允许往线性表的一端放入数据,之后在这一端取出数据,按照后进先出(LIFO, Last In First Out )的顺序,如图所示。

往栈中放入元素的过程叫做入栈。入栈会增加栈的元素数量,最后放入的元素总是位于栈的顶部,最先放入的元素总是位于栈的底部。

从栈中取出元素时,只能从栈顶部取出。取出元素后,栈的数量会变少。最先放入的元素总是最后被取出,最后放入的元素总是最先被取出。不允许从栈底获取数据,也不允许对栈成员(除栈顶外的成员)进行任何查看和修改操作 。

栈内存由编译器自动分配和释放,开发者无法控制。栈内存一般存储函数中的局部变量、参数等,函数创建的时候,这些内存会被自动创建;函数返回的时候,这些内存会被自动释放。

栈可用于内存分配,栈的分配和回收速度非常快。下面代码展示枝在内存分配上的作用,代码如下:

func calc(a, b int) int {
    var c int
    c = a * b

    var x int
    x = c * 10

    return x
}

上面的代码在没有任何优化情况下,会进行 c 和 x 变量的分配过程 。 Go 语言默认情况下会将 c 和 x 分配在栈上,这两个变量在 calc() 函数退出时就不再使用,函数结束时,保存 c 和 x 的栈内存再出栈释放内存,整个分配内存的过程通过栈的分配和回收都会非常迅速。

2. 堆内存
堆在内存分配中类似于往一个房间里摆放各种家具,家具的尺寸有大有小。

分配内存时,需要找一块足够装下家具的空间再摆放家具。经过反复摆放和腾空家具后,房间里的空间会变得乱七八糟,此时再往空间里摆放家具会存在虽然有足够的空间,但各空间分布在不同的区域,无法有一段连续的空间来摆放家具的问题。

此时内存分配器就需要对这些空间进行调整优化,如图所示。

堆分配内存和栈分配内存相比,堆适合不可预知大小的内存分配。但是为此付出的代价是分配速度较慢,而且会形成内存碎片 。

堆内存的生命周期比栈内存要长,如果函数返回的值还会在其他地方使用,那么这个值就会被编译器自动分配到堆上。堆内存相比栈内存来说,不能自动被编译器释放,只能通过垃圾回收器才能释放,所以栈内存效率会很高。

2. 逃逸分析
既然栈内存的效率更高,肯定是优先使用栈内存。那么 Go 语言是如何判断一个变量应该分配到堆上还是栈上的呢?这就需要逃逸分析了。下面我通过一个示例来讲解逃逸分析,代码如下:

package main

func main() {
    newString()
}

func newString() *string{
   s:=new(string)
   *s = "wohu"
   return s
}


现在我通过逃逸分析来看下是否发生了逃逸,命令如下:

wohu@ubuntu:~/gocode/src$ go build -gcflags="-m -l" demo.go
# command-line-arguments
./demo.go:12:10: new(string) escapes to heap
wohu@ubuntu:~/gocode/src$ 

-m 表示打印出逃逸分析信息;
-l 表示禁止内联,可以更好地观察逃逸;
从以上输出结果可以看到,发生了逃逸,也就是说指针作为函数返回值的时候,一定会发生逃逸。

逃逸到堆内存的变量不能马上被回收,只能通过垃圾回收标记清除,增加了垃圾回收的压力,所以要尽可能地避免逃逸,让变量分配在栈内存上,这样函数返回时就可以回收资源,提升效率。

下面我对 newString 函数进行了避免逃逸的优化,优化后的函数代码如下:

func newString() string{
   s:=new(string)
   *s = "wohu"
   return *s
}

再次通过命令查看以上代码的逃逸分析,命令如下:

wohu@ubuntu:~/gocode/src$ go build -gcflags="-m -l" demo.go
# command-line-arguments
./demo.go:8:10: newString new(string) does not escape
wohu@ubuntu:~/gocode/src$ 

通过分析结果可以看到,虽然还是声明了指针变量 s,但是函数返回的并不是指针,所以没有发生逃逸。

逃逸分析是判断变量是分配在堆上还是栈上的一种方法,在实际的项目中要尽可能避免逃逸,这样就不会被 GC 拖慢速度,从而提升效率。

小技巧:从逃逸分析来看,指针虽然可以减少内存的拷贝,但它同样会引起逃逸,所以要根据实际情况选择是否使用指针。

取地址发生逃逸

package main

import "fmt"

type Data struct {
}

func demo() *Data {
    var d Data
    return &d
}

func main() {
    fmt.Println(demo())
}


执行结果:

wohu@wohu-dev:~/gocode/src$ go run -gcflags "-m -l" temp.go 
# command-line-arguments
./temp.go:9:6: moved to heap: d
./temp.go:14:13: main ... argument does not escape
./temp.go:14:18: demo() escapes to heap
&{}
wohu@wohu-dev:~/gocode/src$ 

moved to heap: d。这句话表示, Go 编译器已经确认如果将 d 变量分配在栈上是无法保证程序最终结果的。如果坚持这样做,demo() 的返回值将是 Data 结构的一个不可预知的内存地址。这种情况一般是 C/C++ 语言中容易犯错的地方 :引用了一个函数局部变量的地址。

Go 语言最终选择将 d 的 Data 结构分配在堆上。然后由垃圾回收器去回收 d 的内存 。

优化技巧

尽可能避免逃逸,因为栈内存效率更高,还不用 GC。比如小对象的传参,array 要比 slice效果好。

如果避免不了逃逸,还是在堆上分配了内存,那么对于频繁的内存申请操作,我们要学会重用内存,比如使用 sync.Pool。

选用合适的算法,达到高性能的目的,比如空间换时间。

小提示:性能优化的时候,要结合基准测试,来验证自己的优化是否有提升。

以上是基于 Go 语言的内存管理机制总结出的 3 个方向的技巧,基于这 3 个大方向基本上可以优化出你想要的效果。除此之外,还有一些小技巧,比如要尽可能避免使用锁、并发加锁的范围要尽可能小、使用 StringBuilder 做 string 和 []byte 之间的转换、defer 嵌套不要太多等等。

最后推荐一个 Go 语言自带的性能剖析的工具 pprof,通过它你可以查看 CPU 分析、内存分析、阻塞分析、互斥锁分析。
————————————————
版权声明:本文为CSDN博主「wohu1104」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/wohu1104/article/details/113815428

slice类型的逃逸分析

与原生类型的逃逸分析相似,当返回指向slice的指针时,slice逃逸;当返回slice时,只有slice中的数据逃逸
slice的逃逸分为slice本身(即SliceHeader)的逃逸分析,和slice中的元素(即SliceHeader中Data指向的地址)的逃逸分析

1. SliceHeader分配在栈上、Data分配在堆上

当SliceHeader分配在栈上,Data既可以分配在栈上也可以分配在堆上

  1. 当Data的空间不足、需要动态扩容时,Data会被分配在堆上
  2. 当初始化slice时,Data所占空间达到64K时,SliceHeader和Data都会被分配在堆上(注意这里的64K边界是在自己的windows和linux机上测试到的,没有找go源码的出处,有可能不准确,理解为Data比较大时会直接分配在堆上比较好。另外除了slice,其他的数据类型如果初始化大小超过某个阈值时,应该也会直接分配在堆上

2. 当SliceHeader分配在堆上,SliceHeader和Data都分配在堆上

上代码


package main
import (
    "reflect"
    "strconv"
    "unsafe"
)
// sl为局部变量,SliceHeader没有逃逸,Data由于动态扩容分配在了堆上
func noEscapeSliceWithDataInHeap() {
    var sl []byte
    println("addr of local(no escape, data in heap) slice = ", &sl)
    printSliceHeader(&sl)
    for i := 0; i < 10; i++ {
        println("append " + strconv.Itoa(i))
        sl = append(sl, byte(i))
        printSliceHeader(&sl)
    }
}
// sl为局部变量,SliceHeader没有逃逸,Data不需要动态扩容,分配在栈上
func noEscapeSliceWithDataInStack() {
    sl := make([]byte, 0, 10) //  noEscapeSliceWithDataInStack make([]byte, 0, 10) does not escape
    println("addr of local(no escape, data in stack) slice = ", &sl)
    printSliceHeader(&sl)
    for i := 0; i < 10; i++ {
        println("append " + strconv.Itoa(i))
        sl = append(sl, byte(i))
        printSliceHeader(&sl)
    }
}
// Data过大,SliceHeader和Data直接分配在堆上
func escapeLargeSlice() {
    sl := make([]byte, 0, 1024*64) //make([]byte, 0, 1024 * 64) escapes to heap
    println("addr of local(escape, data in heap) slice = ", &sl)
    printSliceHeader(&sl)
    for i := 0; i < 10; i++ {
        println("append " + strconv.Itoa(i))
        sl = append(sl, byte(i))
        printSliceHeader(&sl)
    }
}
// Data没有达到64k,没有逃逸
func noescapeSmallSlice() {
    sl := make([]byte, 0, 1024*63+1023) // noescapeSmallSlice make([]byte, 0, 1024 * 63 + 1023) does not escape
    println("addr of local(no escape, data in stack) slice = ", &sl)
    printSliceHeader(&sl)
    for i := 0; i < 10; i++ {
        println("append " + strconv.Itoa(i))
        sl = append(sl, byte(i))
        printSliceHeader(&sl)
    }
}
// 返回了sl的指针,SliceHeader和Data都逃逸了
func escapeSliceWithDataInHeap() *[]byte {
    sl := make([]byte, 0, 10) //moved to heap: sl  // make([]byte, 0, 5) escapes to heap
    println("addr of local(slice and data in heap) slice = ", &sl)
    printSliceHeader(&sl)
    for i := 0; i < 10; i++ {
        println("append " + strconv.Itoa(i))
        sl = append(sl, byte(i))
        printSliceHeader(&sl)
    }
    return &sl
}
// sl作为返回值,SliceHeader没有逃逸,Data逃逸
func sliceInStackWithDataInHeap() []byte {
    sl := make([]byte, 0, 10) //make([]byte, 0, 5) escapes to heap
    println("addr of local(slice in stack and data in heap) slice = ", &sl)
    printSliceHeader(&sl)
    for i := 0; i < 10; i++ {
        println("append " + strconv.Itoa(i))
        sl = append(sl, byte(i))
        printSliceHeader(&sl)
    }
    return sl
}
func printSliceHeader(p *[]byte) {
    ph := (*reflect.SliceHeader)(unsafe.Pointer(p))
    println("slice data = ", unsafe.Pointer(ph.Data))
}
func main() {
    noEscapeSliceWithDataInHeap()
    noEscapeSliceWithDataInStack()
    escapeLargeSlice()
    noescapeSmallSlice()
    escapeSliceWithDataInHeap()
    sliceInStackWithDataInHeap()
}


执行编译,并且附加-m内存逃逸分析标志和-l禁止内联标志
go build -gcflags "-m -l" main.go
结果如下:

.\main.go:83:23: printSliceHeader p does not escape
.\main.go:15:21: noEscapeSliceWithDataInHeap "append " + strconv.Itoa(i) does not escape
.\main.go:23:12: noEscapeSliceWithDataInStack make([]byte, 0, 10) does not escape
.\main.go:27:21: noEscapeSliceWithDataInStack "append " + strconv.Itoa(i) does not escape
.\main.go:35:12: make([]byte, 0, 1024 * 64) escapes to heap
.\main.go:39:21: escapeLargeSlice "append " + strconv.Itoa(i) does not escape
.\main.go:47:12: noescapeSmallSlice make([]byte, 0, 1024 * 63 + 1023) does not escape
.\main.go:51:21: noescapeSmallSlice "append " + strconv.Itoa(i) does not escape
.\main.go:59:2: moved to heap: sl
.\main.go:59:12: make([]byte, 0, 10) escapes to heap
.\main.go:63:21: escapeSliceWithDataInHeap "append " + strconv.Itoa(i) does not escape
.\main.go:72:12: make([]byte, 0, 10) escapes to heap
.\main.go:76:21: sliceInStackWithDataInHeap "append " + strconv.Itoa(i) does not escape

map类型的逃逸分析

1. 不作为函数返回值时,分配在栈上

2. 作为函数返回值且返回的不是指针时,map的元素分配在堆上,map本身分配在栈上

3. 作为函数返回值且返回的是指针时,map的元素分配在堆上,map本身也分配在堆上

上代码


package main
func noEscapeMap() {
    sm := make(map[int]int) //noEscapeMap make(map[int]int) does not escape
    sm[1] = 1
}
func escapeMap() map[int]int {
    sm := make(map[int]int) // make(map[int]int) escapes to heap
    sm[1] = 1
    return sm
}
func escapeMapPointer() *map[int]int {
    sm := make(map[int]int) //  moved to heap: sm //  make(map[int]int) escapes to heap
    sm[1] = 1
    return &sm
}
func main() {
    noEscapeMap()
    escapeMap()
}


执行编译,并且附加-m内存逃逸分析标志和-l禁止内联标志
go build -gcflags "-m -l" main.go
结果如下:

.\main.go:4:12: noEscapeMap make(map[int]int) does not escape
.\main.go:9:12: make(map[int]int) escapes to heap
.\main.go:15:2: moved to heap: sm
.\main.go:15:12: make(map[int]int) escapes to heap

三、后话

最后祭出go逃逸分析要遵循的两个不变性:

  1. 指向栈对象的指针不能存在于堆中
  2. 指向栈对象的指针不能在栈对象回收后存活

经过上面的分析和测试,这两条不变性也就很好理解了,懂得都懂!

友情提示

内存逃逸go在编译的时候就已经为我们优化好了,学习它是为了将性能提高至极致以及熟悉一些稍底层的知识。普通的代码不需要在逃不逃逸上杠。



作者:银角代王
链接:https://www.jianshu.com/p/2ad3c68b17a3
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值