

我要在栈上。不,你应该在堆上
source link: https://www.tuicool.com/articles/aIfequa
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

原文地址: 我要在栈上。不,你应该在堆上
前言
我们在写代码的时候,有时候会想这个变量到底分配到哪里了?这时候可能会有人说,在栈上,在堆上。信我准没错...
但从结果上来讲你还是一知半解,这可不行,万一被人懵了呢。今天我们一起来深挖下 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 build -gcflags '-m -l'
之前就有想过要不要写 “逃逸分析” 相关的文章,直到最近看到在夜读里有人问,还是有写的必要。对于这块的知识点。我的建议是适当了解,但没必要硬记。靠基础知识点加命令调试观察就好了。像是曹大之前讲的 “你琢磨半天逃逸分析,一压测,瓶颈在锁上”,完全没必要过度在意...
参考
Recommend
-
181
404 此处为世界尽头请穿越到“界面”首页吧 可能因为: 网址有错误>请检查地址是否完整或存在多余字符 网址已失效>可能页面已删除 返回首页
-
94
设计师说,我们要在 App 里用十种字体!!! Original 承香墨...
-
80
亚马逊想要在你不在家时“送货进门”,你会同意么?36氪的朋友们·2017-10-26 23:21这个服务将于 11 月 7 日正式上线编者按:本文来自...
-
86
Eric Schmidt 认为美国需要在 AI 上共同行动迎接中国的挑战 solidot新版网站常见问题,请点击这里查看。
-
65
Elon Musk 在发表他们 Tesla 的 Master Plan 时,提及到轻型卡车款是其中一个重要的组成元素(你看,官图也画好了)。不过到底在什么时候才会看到它的尾灯呢?Elon Musk 最近在回应 Twitter 追踪者的问题时,就「发誓」要完成预计在 2019 - 2020 年推出 Model Y 后...
-
91
2018-01-12 07:39 ZARA、优衣库们要在中国背水一战?虎嗅注:本文转自“时尚头条网”旗下微信公众号“LADYMAX”(ID:lmfashi...
-
91
程序员 - @crossoverJie - 有内部小伙伴知道具体情况嘛?主要是有没有研发中
-
50
为什么我要在2018年学习Python?
-
60
Chobani要在全美国送免费酸奶,诞生10年后品牌如何重塑?
-
36
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK