摘要:原文地址我要在栈上。很核心的一点就是它有没有被作用域之外所引用,而这里作用域仍然保留在中,因此它没有发生逃逸案例二未确定类型执行命令观察一下,如下通过查看分析结果,可得知变量逃到了堆上,也就是该对象在堆上分配。
原文地址:我要在栈上。不,你应该在堆上
前言我们在写代码的时候,有时候会想这个变量到底分配到哪里了?这时候可能会有人说,在栈上,在堆上。信我准没错...
但从结果上来讲你还是一知半解,这可不行,万一被人懵了呢。今天我们一起来深挖下 Go 在这块的奥妙,自己动手丰衣足食
问题type User struct { ID int64 Name string Avatar string } func GetUserInfo() *User { return &User{ID: 13746731, Name: "EDDYCJY", Avatar: "https://avatars0.githubusercontent.com/u/13746731"} } func main() { _ = GetUserInfo() }
开局就是一把问号,带着问题进行学习。请问 main 调用 GetUserInfo 后返回的 &User{...}。这个变量是分配到栈上了呢,还是分配到堆上了?
什么是堆/栈在这里并不打算详细介绍堆栈,仅简单介绍本文所需的基础知识。如下:
堆(Heap):一般来讲是人为手动进行管理,手动申请、分配、释放。一般所涉及的内存大小并不定,一般会存放较大的对象。另外其分配相对慢,涉及到的指令动作也相对多
栈(Stack):由编译器进行管理,自动申请、分配、释放。一般不会太大,我们常见的函数参数(不同平台允许存放的数量不同),局部变量等等都会存放在栈上
今天我们介绍的 Go 语言,它的堆栈分配是通过 Compiler 进行分析,GC 去管理的,而对其的分析选择动作就是今天探讨的重点
什么是逃逸分析在编译程序优化理论中,逃逸分析是一种确定指针动态范围的方法,简单来说就是分析在程序的哪些地方可以访问到该指针
通俗地讲,逃逸分析就是确定一个变量要放堆上还是栈上,规则如下:
是否有在其他地方(非局部)被引用。只要有可能被引用了,那么它一定分配到堆上。否则分配到栈上
即使没有被外部引用,但对象过大,无法存放在栈区上。依然有可能分配到堆上
对此你可以理解为,逃逸分析是编译器用于决定变量分配到堆上还是栈上的一种行为
在什么阶段确立逃逸在编译阶段确立逃逸,注意并不是在运行时
为什么需要逃逸这个问题我们可以反过来想,如果变量都分配到堆上了会出现什么事情?例如:
垃圾回收(GC)的压力不断增大
申请、分配、回收内存的系统开销增大(相对于栈)
动态分配产生一定量的内存碎片
其实总的来说,就是频繁申请、分配堆内存是有一定 “代价” 的。会影响应用程序运行的效率,间接影响到整体系统。因此 “按需分配” 最大限度的灵活利用资源,才是正确的治理之道。这就是为什么需要逃逸分析的原因,你觉得呢?
怎么确定是否逃逸第一,通过编译器命令,就可以看到详细的逃逸分析过程。而指令集 -gcflags 用于将标识参数传递给 Go 编译器,涉及如下:
-m 会打印出逃逸分析的优化策略,实际上最多总共可以用 4 个 -m,但是信息量较大,一般用 1 个就可以了
-l 会禁用函数内联,在这里禁用掉 inline 能更好的观察逃逸情况,减少干扰
$ go build -gcflags "-m -l" main.go
第二,通过反编译命令查看
$ go tool compile -S main.go
注:可以通过 go tool compile -help 查看所有允许传递给编译器的标识参数
逃逸案例 案例一:指针第一个案例是一开始抛出的问题,现在你再看看,想想,如下:
type User struct { ID int64 Name string Avatar string } func GetUserInfo() *User { return &User{ID: 13746731, Name: "EDDYCJY", Avatar: "https://avatars0.githubusercontent.com/u/13746731"} } func main() { _ = GetUserInfo() }
执行命令观察一下,如下:
$ go build -gcflags "-m -l" main.go # command-line-arguments ./main.go:10:54: &User literal escapes to heap
通过查看分析结果,可得知 &User 逃到了堆里,也就是分配到堆上了。这是不是有问题啊...再看看汇编代码确定一下,如下:
$ go tool compile -S main.go "".GetUserInfo STEXT size=190 args=0x8 locals=0x18 0x0000 00000 (main.go:9) TEXT "".GetUserInfo(SB), $24-8 ... 0x0028 00040 (main.go:10) MOVQ AX, (SP) 0x002c 00044 (main.go:10) CALL runtime.newobject(SB) 0x0031 00049 (main.go:10) PCDATA $2, $1 0x0031 00049 (main.go:10) MOVQ 8(SP), AX 0x0036 00054 (main.go:10) MOVQ $13746731, (AX) 0x003d 00061 (main.go:10) MOVQ $7, 16(AX) 0x0045 00069 (main.go:10) PCDATA $2, $-2 0x0045 00069 (main.go:10) PCDATA $0, $-2 0x0045 00069 (main.go:10) CMPL runtime.writeBarrier(SB), $0 0x004c 00076 (main.go:10) JNE 156 0x004e 00078 (main.go:10) LEAQ go.string."EDDYCJY"(SB), CX ...
我们将目光集中到 CALL 指令,发现其执行了 runtime.newobject 方法,也就是确实是分配到了堆上。这是为什么呢?
分析结果这是因为 GetUserInfo() 返回的是指针对象,引用被返回到了方法之外了。因此编译器会把该对象分配到堆上,而不是栈上。否则方法结束之后,局部变量就被回收了,岂不是翻车。所以最终分配到堆上是理所当然的
再想想那你可能会想,那就是所有指针对象,都应该在堆上?并不。如下:
func main() { str := new(string) *str = "EDDYCJY" }
你想想这个对象会分配到哪里?如下:
$ go build -gcflags "-m -l" main.go # command-line-arguments ./main.go:4:12: main new(string) does not escape
显然,该对象分配到栈上了。很核心的一点就是它有没有被作用域之外所引用,而这里作用域仍然保留在 main 中,因此它没有发生逃逸
案例二:未确定类型func main() { str := new(string) *str = "EDDYCJY" fmt.Println(str) }
执行命令观察一下,如下:
$ go build -gcflags "-m -l" main.go # command-line-arguments ./main.go:9:13: str escapes to heap ./main.go:6:12: new(string) escapes to heap ./main.go:9:13: main ... argument does not escape
通过查看分析结果,可得知 str 变量逃到了堆上,也就是该对象在堆上分配。但上个案例时它还在栈上,我们也就 fmt 输出了它而已。这...到底发生了什么事?
分析结果相对案例一,案例二只加了一行代码 fmt.Println(str),问题肯定出在它身上。其原型:
func Println(a ...interface{}) (n int, err error)
通过对其分析,可得知当形参为 interface 类型时,在编译阶段编译器无法确定其具体的类型。因此会产生逃逸,最终分配到堆上
如果你有兴趣追源码的话,可以看下内部的 reflect.TypeOf(arg).Kind() 语句,其会造成堆逃逸,而表象就是 interface 类型会导致该对象分配到堆上
案例三、泄露参数type User struct { ID int64 Name string Avatar string } func GetUserInfo(u *User) *User { return u } func main() { _ = GetUserInfo(&User{ID: 13746731, Name: "EDDYCJY", Avatar: "https://avatars0.githubusercontent.com/u/13746731"}) }
执行命令观察一下,如下:
$ go build -gcflags "-m -l" main.go # command-line-arguments ./main.go:9:18: leaking param: u to result ~r1 level=0 ./main.go:14:63: main &User literal does not escape
我们注意到 leaking param 的表述,它说明了变量 u 是一个泄露参数。结合代码可得知其传给 GetUserInfo 方法后,没有做任何引用之类的涉及变量的动作,直接就把这个变量返回出去了。因此这个变量实际上并没有逃逸,它的作用域还在 main() 之中,所以分配在栈上
再想想那你再想想怎么样才能让它分配到堆上?结合案例一,举一反三。修改如下:
type User struct { ID int64 Name string Avatar string } func GetUserInfo(u User) *User { return &u } func main() { _ = GetUserInfo(User{ID: 13746731, Name: "EDDYCJY", Avatar: "https://avatars0.githubusercontent.com/u/13746731"}) }
执行命令观察一下,如下:
$ go build -gcflags "-m -l" main.go # command-line-arguments ./main.go:10:9: &u escapes to heap ./main.go:9:18: moved to heap: u
只要一小改,它就考虑会被外部所引用,因此妥妥的分配到堆上了
总结在本文我给你介绍了逃逸分析的概念和规则,并列举了一些例子加深理解。但实际肯定远远不止这些案例,你需要做到的是掌握方法,遇到再看就好了。除此之外你还需要注意:
静态分配到栈上,性能一定比动态分配到堆上好
底层分配到堆,还是栈。实际上对你来说是透明的,不需要过度关心
每个 Go 版本的逃逸分析都会有所不同(会改变,会优化)
直接通过 go build -gcflags "-m -l" 就可以看到逃逸分析的过程和结果
到处都用指针传递并不一定是最好的,要用对
之前就有想过要不要写 “逃逸分析” 相关的文章,直到最近看到在夜读里有人问,还是有写的必要。对于这块的知识点。我的建议是适当了解,但没必要硬记。靠基础知识点加命令调试观察就好了。像是曹大之前讲的 “你琢磨半天逃逸分析,一压测,瓶颈在锁上”,完全没必要过度在意...
参考Golang escape analysis
FAQ
逃逸分析
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/31543.html
摘要:源文件路径版本主要作用分析是内部使用的数组型数据结构,与语言内置的数组概念上类似,但是有两点主要区别使用内存池来管理内存虽然有预设数组大小的概念,但是在数组元素超出预设值大小时,会在内存池中发生重分配。 源文件路径 版本:1.8.0 srccoreNgx_array.h srccoreNgx_array.c 主要作用分析 ngx_array_t是Nginx内部使用的数组型数据...
摘要:方法区在实际内存空间站可以是不连续的。这一规定,可以说是给了虚拟机厂商很大的自由。但是值得注意的是,堆其实还未每一个线程单独分配了一块空间,这部分空间在分配时是线程独享的,在使用时是线程共享的。 在我的博客中,之前有很多文章介绍过JVM内存结构,相信很多看多我文章的朋友对这部分知识都有一定的了解了。 那么,请大家尝试着回答一下以下问题: 1、JVM管理的内存结构是怎样的? 2、不同的...
摘要:堆内存主要作用是存放运行时创建的对象。堆内存用来存放由创建的对象和数组,在堆中分配的内存,由虚拟机的自动垃圾回收器来管理。这也是比较占内存的原因,实际上,栈中的变量指向堆内存中的变量,这就是中的指针 堆:(对象) 引用类型的变量,其内存分配在堆上或者常量池(字符串常量、基本数据类型常量),需要通过new等方式来创建。 堆内存主要作用是存放运行时创建(new)的对象。(主要用于存放对象,...
阅读 2847·2021-11-22 15:22
阅读 19006·2021-09-22 15:00
阅读 1432·2021-09-07 09:58
阅读 1235·2019-08-30 13:01
阅读 2408·2019-08-29 16:27
阅读 2344·2019-08-26 13:25
阅读 1618·2019-08-26 12:13
阅读 934·2019-08-26 11:53