本文转载自微信公众号《我的脑子是炸鱼》,作者陈建宇。转载本文请联系脑筋急转弯公众号。大家好,我是学蒸鱼的建宇。我们在写代码的时候,有时候会想这个变量分配到哪里去了?这时候可能有人会说,在栈上还是在堆上。相信我,你是对的……但就结果而言,你还是有点知识,这不好,以防有人一头雾水。今天就让我们在这方面深挖围棋的奥秘,自己动手吧!问题类型Userstruct{IDint64NamestringAvatarstring}funcGetUserInfo()*User{return&User{ID:13746731,Name:"EDDYCJY",Avatar:"https://avatars0.githubusercontent.com/u/13746731"}}funcmain(){_=GetUserInfo()}开头是问号,带着疑问学习。请问调用GetUserInfo后main返回的&User{...}。这个变量是分配在栈上还是堆上?什么是堆/栈?这里我不打算详细介绍栈,只是简单介绍一下本文所需要的基础知识。如下:堆(Heap):一般来说是人工管理的,人工申请、分配、释放。一般涉及的内存大小不固定,一般存放较大的对象。另外,它的分配速度比较慢,涉及的指令动作也比较多。堆栈(Stack):由编译器管理,自动申请、分配、释放。一般不会太大,我们常用的函数参数(不同平台允许存放的个数不同)、局部变量等都会存放在栈中。在我们今天介绍的Go语言中,它的栈分配是由Compiler来分析,由GC来管理的,而对于它的action的分析和选择是今天讨论的重点。什么是逃逸分析?在编译器优化理论中,逃逸分析是一种确定指针动态范围的方法。简单的说就是分析指针在程序中哪里可以访问到。通俗地说,逃逸分析就是判断一个变量应该放在堆上还是栈上。规则如下:是否在别处(非本地)引用。只要有可能被引用,就一定分配在堆上。否则分配在堆栈上。即使不被外部引用,这个对象也太大了,无法存放在栈中。仍然可以在堆上分配。您可以将此理解为逃逸分析是编译器用来确定变量是在堆上还是在堆栈上分配的行为。转义是在什么阶段建立在编译阶段,而不是在运行时。为什么我们需要逃跑?我们可以反过来想,如果把变量分配到堆上会怎样?例如:垃圾回收(GC)的压力不断增加。应用、分配和回收内存的系统开销增加(相对于堆栈)。动态分配会产生一定数量的内存碎片。简单来说,频繁申请和分配堆内存是有一定“成本”的。它会影响应用程序的运行效率,间接影响整个系统。因此,“按需分配”,最大限度地灵活使用资源,才是正确的治理之道。这就是为什么需要逃逸分析,你怎么看?如何判断是否逃逸?首先,通过编译命令,可以看到详细的逃逸分析过程。指令集-gcflags用于将识别参数传递给Go编译器,其中涉及到:-m会打印出逃逸分析的优化策略。其实总共最多可以用4个-m,但是信息量大,一般用1个。只有一个。-l会禁用函数内联,这里禁用内联可以更好的观察逃逸情况,减少干扰。$gobuild-gcflags'-m-l'main.go二、通过反编译命令查看$gotoolcompile-Smain.go注意:可以通过gotoolcompile-help查看所有允许传递给编译器的标识参数.EscapeCaseCase1:Pointer第一种情况就是一开始提出的问题,现在你看,想一想,如下:",Avatar:"https://avatars0.githubusercontent.com/u/13746731"}}funcmain(){_=GetUserInfo()}执行命令观察,如下:$gobuild-gcflags'-m-l'main.go#command-line-arguments./main.go:10:54:&Userliteralescapestoheap通过查看分析结果可以知道&User已经逃到了堆上,也就是在堆上分配了。是不是有问题……看汇编代码确定一下,如下:$gotoolcompile-Smain.go"".GetUserInfoSTEXTsize=190args=0x8locals=0x180x000000000(main.go:9)TEXT"".GetUserInfo(SB),$24-8...0x002800040(main.go:10)MOVQAX,(SP)0x002c00044(main.go:10)CALLruntime.newobject(SB)0x003100049(main.go:10)PCDATA$2,$10x003100049(main.go:10)MOVQ8(SP),AX0x003600054(main.go:10)MOVQ$13746731,(AX)0x003d00061(main.go:10)MOVQ$7,16(AX)0x004500069(main.go:10)PCDATA$2,$-20x004500069(main.go:10)PCDATA$0,$-20x004500069(main.go:10)CMPLruntime.writeBarrier(SB),$00x004c00076(main.go:10)JNE1560x004e00078(main.go:10)LEAQgo.string."EDDYCJY"(SB),CX...我们关注CALL指令,发现它执行了runtime.newobject方法,也就是说,它确实分配在了堆上。为什么是这样?分析结果是因为GetUserInfo()返回的是一个指针对象,在方法外返回了引用。所以编译器会在堆上分配对象,而不是在栈上。不然方法结束后,局部变量会被回收,那岂不是崩溃了。所以最后分配到堆上是理所当然的事情。那么你可能会想,也就是说,所有的指针对象都应该在堆上吗?不。如下:funcmain(){str:=new(string)*str="EDDYCJY"}你觉得这个对象会分配到哪里呢?如下:$gobuild-gcflags'-m-l'main.go#command-line-arguments./main.go:4:12:mainnew(string)doesnotescape很明显,对象是在栈上分配的。核心点是是否在作用域外被引用,这里的作用域还是在main里面,所以没有逃逸。Case2:Undeterminedtypefuncmain(){str:=new(string)*str="EDDYCJY"fmt.Println(str)}执行命令观察,如下:$gobuild-gcflags'-m-l'main。go#command-line-arguments./main.go:9:13:strescapestoheap./main.go:6:12:new(string)escapestoheap./main.go:9:13:main...argumentdoesnotescape通过查看分析结果可以看出,str变量已经逃逸到了堆中,也就是对象分配到了堆上。但是上次的时候还是在栈上的,所以我们直接从fmt中输出就可以了。这……这是怎么回事?与Case1相比,Case2只增加了一行代码fmt.Println(str),问题一定出在这上面。其原型:funcPrintln(a...interface{})(nint,errerror)通过对其分析可知,当形参为接口类型时,编译器无法在编译阶段确定其具体类型。因此,就会出现逃逸,最终会分配到堆上。有兴趣追源码的可以看看内部的reflect.TypeOf(arg).Kind()语句,会造成堆逃逸,外观是接口类型会导致对象分配在堆。案例3.泄露参数typeUserstruct{IDint64NamestringAvatarstring}funcGetUserInfo(u*User)*User{returnu}funcmain(){_=GetUserInfo(&User{ID:13746731,Name:"EDDYCJY",Avatar:"https://avatars0.githubusercontent.com/u/13746731"})}执行命令观察,如下:$gobuild-gcflags'-m-l'main.go#command-line-arguments./main.go:9:18:leakingparam:utoresult~r1level=0./main.go:14:63:main&Userliteraldoesnotescape我们注意到泄漏参数的表达式,这表明变量u是泄漏参数。结合代码可知,传递给GetUserInfo方法后,变量直接返回,没有任何涉及变量的引用等动作。所以这个变量实际上并没有逃逸,它的作用域还在main()中,所以分配在栈上。再想一想,再想想怎么分配到堆上?结合案例1,举一反三。修改如下:typeUserstruct{IDint64NamestringAvatarstring}funcGetUserInfo(uUser)*User{return&u}funcmain(){_=GetUserInfo(User{ID:13746731,Name:"EDDYCJY",Avatar:"https://avatars0.githubusercontent.com/u/13746731"})}执行命令观察,如下:$gobuild-gcflags'-m-l'main.go#command-line-arguments./main.go:10:9:&uescapestoheap./main.go:9:18:movedtoheap:u只要有一点点变化,就会认为是被外部引用了,所以会适当分配到堆中。总结在本文中,我介绍了逃逸分析的概念和规则,并列举了一些例子来加深理解。但现实肯定远不止这些案例。你需要做的是掌握方法,遇到了就看看。另外需要注意:静态分配到栈上,性能肯定比动态分配到堆上好。底层分配给堆或栈。其实对你来说是透明的,你不用太在意。每个Go版本的逃逸分析都会有所不同(更改、优化)。通过gobuild-gcflags'-m-l'可以直接看到逃逸分析的过程和结果。处处传指针不一定是最好的,用对了。这块知识。我的建议是适当理解,但没必要死记硬背,因为Go语言每次升级都可能发生变化。就靠基础知识点加命令调试观察。正如曹大之前所说的“你想逃逸分析很久了,经过压力测试,瓶颈就被锁住了”,不用太在意……参考Golang逃逸分析FAQEscape分析
