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

网友投稿 254 2022-08-24

爱上开源之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.gowarning: 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逃逸分析要遵循的两个本质特点:

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

常见的内存逃逸场景

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

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

对象太大, 超过栈帧大小

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

闭包引用逃逸

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

动态类型逃逸

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

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

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

基于内存逃逸的思考

对于性能要求比较高且访问频次比较高的函数调用,应该尽量避免使用接口类型。不要盲目使用变量指针作为参数,虽然减少了复制,但变量逃逸的开销更大。预先设定好slice长度,避免频繁超出容量,重新分配。

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

以下是总结出来的关键点

堆上动态分配内存比栈上静态分配内存,开销大很多。变量分配在栈上需要能在编译期确定它的作用域,否则会分配到堆上。Go编译器会在编译期对考察变量的作用域,并作一系列检查,如果它的作用域在运行期间对编译器一直是可知的,那么就会分配到栈上。简单来说,编译器会根据变量是否被外部引用来决定是否逃逸。对于Go程序员来说,编译器的这些逃逸分析规则不需要掌握,我们只需通过go build -gcflags '-m'命令来观察变量逃逸情况就行了。不要盲目使用变量的指针作为函数参数,虽然它会减少复制操作。但其实当参数为变量自身的时候,复制是在栈上完成的操作,开销远比变量逃逸后动态地在堆上分配内存少的多。最后,尽量写出少一些逃逸的代码,提升程序的运行效率。

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:爱上开源之golang入门至实战第四章-数据基本类型
下一篇:地产线上营销,被他们玩明白了!(房地产营销噱头)
相关文章

 发表评论

暂时没有评论,来抢沙发吧~