Golang - 内存逃逸详解

一、逃逸分析

内存逃逸分析是编译器在编译优化时,用来决定变量应该分配在堆上还是栈上的工具。

  • 了解内存逃逸分析的原理后,就能够理解什么样的变量会被分配在栈上、什么样的变量会被分配在堆上。
  • 当你的程序对性能非常敏感,就可以使用内存逃逸工具的分析,查看哪些变量逃逸到了堆上,哪些没有逃逸。然后调整代码,再次提高你的程序性能。

go在编译时会进行内存逃逸分析,同样也给开发人员开放了内存逃逸信息。

在编译时增加-m标志,就会输出内存逃逸信息。

go build -gcflags="-m"

二、内存逃逸的影响

内存逃逸会对程序的性能产生影响。当一个变量逃逸到堆上时,会增加垃圾回收的压力,并且会导致额外的堆分配和内存拷贝操作。相比之下,栈上分配的变量更加高效,因为它们的生命周期受限于函数的执行时间。

逃逸变量和非逃逸变量的区别在于它们的分配位置和生命周期。逃逸变量分配在堆上,生命周期可能会比函数的执行时间长;而非逃逸变量分配在栈上,生命周期与函数的执行时间一致。

三、几种常见的内存逃逸情况

原生类型的逃逸分析

1)变量分配在栈上的情况

当原生类型被取地址且地址被赋值给了一个指针变量,当这个指针变量只是在函数内部使用,则这个原生类型会被分配在栈上(即使是通过new方法分配的)

2)变量逃逸到堆上的情况

如果这个指针变量被以某种形式作为了函数返回值(例如,指针变量是struct中的变量,struct是函数返回值),则这个原生类型被分配在堆上(原因很简单,如果分配在栈上,函数返回后栈中的数据失效,这个指针指向的地址就是无效的)

复制代码
package main
type (
    Foo struct {
        A int
        B string
    }
    FooHasPointer struct {
        A *int
        B string
    }
)
// 返回了指向了a的指针,a逃逸到堆上
func escapeValue() *int {
    var a int // moved to heap: a
    a = 1
    return &a
}
// 即使newa是指针类型,但是它只在本函数内起作用(没有被作为返回值,相当于一个局部变量),分配到栈上
func noescapeNew() {
    newa := new(int) // noescapeNew new(int) does not escape
    *newa = 1
}
// 指向i的指针被存储到foo结构体中返回了,i逃逸到堆上
func escapePointer() FooHasPointer {
    var foo FooHasPointer
    i := 10 //moved to heap: i
    foo.A = &i
    foo.B = "a"
    return foo
}
// 没有指针,都分配到栈上
func noescapeValue() Foo {
    var foo Foo
    i := 10
    foo.A = i
    foo.B = "a"
    return foo
}
func main() {
}
复制代码

执行编译,并且附加-m内存逃逸分析标志和-l(L的小写)禁止内联标志

go build -gcflags "-m -l" main.go

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

结果如下:

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

结果如下:

四、如何避免内存逃逸

为了避免内存逃逸,我们可以采取一些实用的技巧和最佳实践:

1)使用局部变量:将变量的作用域限制在函数内部,避免变量逃逸到堆上。
2)使用栈分配:尽量使用栈分配而不是堆分配,因为栈分配的效率更高。
3)避免不必要的堆分配:尽量减少不必要的对象分配,可以通过复用对象或使用对象池来减少堆分配的次数。

五、内存逃逸的优化

为了减少内存逃逸,可以通过优化代码来改善性能:

1)使用指针传递:将大的结构体或数组通过指针传递,避免复制数据。【需要修改原对象值,或占用内存比较大的结构体】
2)使用值传递:对于小的结构体或基本类型,可以使用值传递,避免指针的额外开销。【对于只读的占用内存较小的结构体】

六、内存逃逸的调试和分析工具

为了调试和分析内存逃逸问题,可以使用一些常用的工具和技术:

1)Go编译器的逃逸分析标志:通过设置编译器的逃逸分析标志,可以查看变量是否逃逸。

2)性能工具:可以使用Go语言自带的pprof工具来进行性能分析,通过查看内存分配和堆栈信息,可以帮助我们找出内存逃逸的问题。

3)可以使用Go语言的vet工具来进行静态分析,它可以检查代码中的内存逃逸问题,并给出相应的警告。

4)还可以使用第三方工具如go-torch和go tool trace来进行更详细的性能分析和调试。

七、总结

最后得出go逃逸分析要遵循的两个不变性:
1、指向栈对象的指针不能存在于堆中
2、指向栈对象的指针不能在栈对象回收后存活

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

posted @   李若盛开  阅读(914)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· DeepSeek 开源周回顾「GitHub 热点速览」
点击右上角即可分享
微信分享提示