爱上开源之golang入门至实战第三章-内存逃逸

 

3.3.3 内存逃逸(memory escape)

如上面我们介绍的,内存的分配可以在堆上也可以在栈上,当然内存在栈上分配更快,并且栈上的内存不需要GC,入栈出栈直接回收。通常情况下,函数的内部中不对外开放的局部变量,并只作用于当前函数中的变量,它的内存是分配在栈中。执行函数前会执行进栈操作,函数结束后会出栈,同时释放内存。 但是由于某种原因,原本是该分配到栈上的变量,跑到了堆上,这就导致了内存逃逸。

 

常见的 go、java 语言都会有内存逃逸的情况,我们常用的函数以及局部变量通常是分配到栈上的,但是一旦出现内存逃逸,变量就会分配到堆上。

逃逸后果

栈是高地址到低地址,栈上的变量,函数结束后变量会跟着回收掉,不会有额外性能的开销。

但是堆上的变量,如果要回收掉,需要进行 gc,那么 gc 一定会带来额外的性能开销,大家知道,编程语言不断优化 gc 算法,主要目的都是为了减少 gc 带来的额外性能开销,高级语言基本都是有 gc 的,除了 rust 一类的;所以变量一旦逃逸会导致性能开销变大,这当然并不是什么好事,但是一般来讲,如果不是什么性能敏感的地方,这点性能完全可以忽略。

所以了解内存逃逸,对我们追踪变量实际分配到那个地方了是有很大帮助的,甚至如果遇到性能调优的地方,这里也是一个优化点。

看看下面的示例代码

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() {
}
​

内存逃逸分析

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

  • 在编译时增加-m标志,如 go build -gcflags="-m -l",就会输出内存逃逸信息

执行编译

/// 并且附加-m内存逃逸分析标志和-l(L的小写)禁止内联标志
go build -gcflags "-m -l" helloworld.go

执行结果

PS E:\WORK\PROJECT\git\go\go-in-practice\code\charpter-01\helloworld> go build -gcflags "-m -l" helloworld.go
warning: GOPATH set to GOROOT (E:\WORK\SOFT\go1.18.windows-amd64\go) has no effect
# command-line-arguments
.\helloworld.go:16:6: moved to heap: a
.\helloworld.go:23:13: new(int) does not escape
.\helloworld.go:30:2: moved to heap: i

 

Golang逃逸分析要遵循的两个本质特点:

  1. 指向栈对象的指针不能存在于堆中

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

常见的内存逃逸场景

  1. 函数内将局部变量指针返回,被外部引用,其生命周期大于栈,溢出

    
    type User struct {}
    ​
    func NewUser() *User{
        user := User{}
        return &user
    }
    ​
    func main() {
        _ = NewUser()
    }

  2. 对象太大, 超过栈帧大小

    
    func main() {
        _ = make([]int, 0, 1000)
        _ = make([]int, 0, 10000)
    }

  3. 闭包引用逃逸

    
    func f() func() int{
        a := 1
        return func() int {
            return a
        }
    }
    ​
    func main() {
        f()
    }

  4. 动态类型逃逸

    func main() {
        a := 1
        fmt.Println("a逃逸,a:", a)
    }

  5. 在切片上存储指针或带指针的值。比如[]*string,导致切片内容逃逸,其引用值一直在堆上

    func main() {
        ch := make(chan  *string, 1)
        ch <- new(string)
    }

基于内存逃逸的思考

  1. 对于性能要求比较高且访问频次比较高的函数调用,应该尽量避免使用接口类型。

  2. 不要盲目使用变量指针作为参数,虽然减少了复制,但变量逃逸的开销更大。

  3. 预先设定好slice长度,避免频繁超出容量,重新分配。

在使用Go语言进行编程时, Go语言的设计者不希望开发者将精力放在内存应该分配在栈还是堆上的问题。编译器会自动帮助开发者完成这个纠结的选择。但变量逃逸分析也是需要了解的一个编译器技术,这个技术不仅用于Go语言,在Java等语言的编译器优化上也使用了类似的技术。

以下是总结出来的关键点

  • 堆上动态分配内存比栈上静态分配内存,开销大很多。

  • 变量分配在栈上需要能在编译期确定它的作用域,否则会分配到堆上。

  • Go编译器会在编译期对考察变量的作用域,并作一系列检查,如果它的作用域在运行期间对编译器一直是可知的,那么就会分配到栈上。简单来说,编译器会根据变量是否被外部引用来决定是否逃逸。

  • 对于Go程序员来说,编译器的这些逃逸分析规则不需要掌握,我们只需通过go build -gcflags '-m'命令来观察变量逃逸情况就行了。

  • 不要盲目使用变量的指针作为函数参数,虽然它会减少复制操作。但其实当参数为变量自身的时候,复制是在栈上完成的操作,开销远比变量逃逸后动态地在堆上分配内存少的多。

  • 最后,尽量写出少一些逃逸的代码,提升程序的运行效率。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

inthirties

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值