该内容是在Go1。6版本的时候写的。Golang经过了多个版本的迭代,逃逸分析机制也在不断的迭代优化,最新版本的效果与文章结论存在一些出入;所以该内容仅供参考。介绍 Go语言较之C语言一个很大的优势就是自带GC功能,可GC并不是没有代价的。写C语言的时候,在一个函数内声明的变量,在函数退出后会自动释放掉,因为这些变量分配在栈上。如果你期望变量的数据可以在函数退出后仍然能被访问,就需要调用malloc方法在堆上申请内存,如果程序不再需要这块内存了,再调用free方法释放掉。Go语言不需要你主动调用malloc来分配堆空间,编译器会自动分析,找出需要malloc的变量,使用堆内存。编译器的这个分析过程就叫做逃逸分析。 所以你在一个函数中通过dict:make(map〔string〕int)创建一个map变量,其背后的数据是放在栈空间上还是堆空间上,是不一定的。这要看编译器分析的结果。 可逃逸分析并不是百分百准确的,它有缺陷。有的时候你会发现有些变量其实在栈空间上分配完全没问题的,但编译后程序还是把这些数据放在了堆上。如果你了解Go语言编译器逃逸分析的机制,在写代码的时候就可以有意识地绕开这些缺陷,使你的程序更高效。关于堆栈 Go语言虽然在内存管理方面降低了编程门槛,即使你不了解堆栈也能正常开发,但如果你要在性能上较真的话,还是要掌握这些基础知识。 这里不对堆内存和栈内存的区别做太多阐述。简单来说就是,栈分配廉价,堆分配昂贵。栈空间会随着一个函数的结束自动释放,堆空间需要时间GC模块不断地跟踪扫描回收。如果对这两个概念有些迷糊,建议阅读下面个文章:LanguageMechanicsOnStacksAndPointersLanguageMechanicsOnEscapeAnalysis 这里举一个小例子,来对比下堆栈的差别:funcstack()int{变量i会在栈上分配i:10returni}funcheap()int{变量j会在堆上分配j:10returnj} stack函数中的变量i在函数退出会自动释放;而heap函数返回的是对变量i的引用,也就是说heap()退出后,表示变量i还要能被访问,它会自动被分配到堆空间上。 他们编译出来的代码如下:gobuildgcflagsltest。gogotoolobjdump。testTEXTmain。stack(SB)tmptest。gotest。go:70x48724048c74424080a000000MOVQ0xa,0x8(SP)test。go:70x487249c3RETTEXTmain。heap(SB)tmptest。gotest。go:90x48725064488b0c25f8ffffffMOVQFS:0xfffffff8,CXtest。go:90x487259483b6110CMPQ0x10(CX),SPtest。go:90x48725d7639JBE0x487298test。go:90x48725f4883ec18SUBQ0x18,SPtest。go:90x48726348896c2410MOVQBP,0x10(SP)test。go:90x487268488d6c2410LEAQ0x10(SP),BPtest。go:100x48726d488d05ac090100LEAQ0x109ac(IP),AXtest。go:100x48727448890424MOVQAX,0(SP)test。go:100x487278e8f33df8ffCALLruntime。newobject(SB)test。go:100x48727d488b442408MOVQ0x8(SP),AXtest。go:100x48728248c7000a000000MOVQ0xa,0(AX)test。go:110x4872894889442420MOVQAX,0x20(SP)test。go:110x48728e488b6c2410MOVQ0x10(SP),BPtest。go:110x4872934883c418ADDQ0x18,SPtest。go:110x487297c3RETtest。go:90x487298e8a380fcffCALLruntime。morestacknoctxt(SB)test。go:90x48729debb1JMPmain。heap(SB)。。。TEXTruntime。newobject(SB)usrsharegosrcruntimemalloc。gomalloc。go:10670x40b07064488b0c25f8ffffffMOVQFS:0xfffffff8,CXmalloc。go:10670x40b079483b6110CMPQ0x10(CX),SPmalloc。go:10670x40b07d763dJBE0x40b0bcmalloc。go:10670x40b07f4883ec28SUBQ0x28,SPmalloc。go:10670x40b08348896c2420MOVQBP,0x20(SP)malloc。go:10670x40b088488d6c2420LEAQ0x20(SP),BPmalloc。go:10680x40b08d488b442430MOVQ0x30(SP),AXmalloc。go:10680x40b092488b08MOVQ0(AX),CXmalloc。go:10680x40b09548890c24MOVQCX,0(SP)malloc。go:10680x40b0994889442408MOVQAX,0x8(SP)malloc。go:10680x40b09ec644241001MOVB0x1,0x10(SP)malloc。go:10680x40b0a3e888f4ffffCALLruntime。mallocgc(SB)malloc。go:10680x40b0a8488b442418MOVQ0x18(SP),AXmalloc。go:10680x40b0ad4889442438MOVQAX,0x38(SP)malloc。go:10680x40b0b2488b6c2420MOVQ0x20(SP),BPmalloc。go:10680x40b0b74883c428ADDQ0x28,SPmalloc。go:10680x40b0bbc3RETmalloc。go:10670x40b0bce87f420400CALLruntime。morestacknoctxt(SB)malloc。go:10670x40b0c1ebadJMPruntime。newobject(SB) 逻辑的复杂度不言而喻,从上面的汇编中可看到,heap()函数调用了runtime。newobject()方法,它会调用mallocgc方法从mcache上申请内存,申请的内部逻辑前面文章已经讲述过。堆内存分配不仅分配上逻辑比栈空间分配复杂,它最致命的是会带来很大的管理成本,Go语言要消耗很多的计算资源对其进行标记回收(也就是GC成本)。 不要以为使用了堆内存就一定会导致性能低下,使用栈内存会带来性能优势。因为实际项目中,系统的性能瓶颈一般都不会出现在内存分配上。千万不要盲目优化,找到系统瓶颈,用数据驱动优化。逃逸分析 Go编辑器会自动帮我们找出需要进行动态分配的变量,它是在编译时追踪一个变量的生命周期,如果能确认一个数据只在函数空间内访问,不会被外部使用,则使用栈空间,否则就要使用堆空间。 我们在gobuild编译代码时,可使用gcflagsm参数来查看逃逸分析日志。gobuildgcflagsmmtest。go 以上面的两个函数为例,编译的日志输出是:tmptest。go:11:9:iescapestoheaptmptest。go:11:9:fromr0(return)attmptest。go:11:2tmptest。go:10:2:movedtoheap:itmptest。go:16:18:heap()escapestoheaptmptest。go:16:18:from。。。argument(argto。。。)attmptest。go:16:13tmptest。go:16:18:from(。。。argument)(indirection)attmptest。go:16:13tmptest。go:16:18:from。。。argument(passedtocall〔argumentcontentescapes〕)attmptest。go:16:13tmptest。go:16:13:main。。。argumentdoesnotescape 日志中的iescapestoheap表示该变量数据逃逸到了堆上。逃逸分析的缺陷 需要使用堆空间,所以逃逸,这没什么可争议的。但编译器有时会将不需要使用堆空间的变量,也逃逸掉。这里是容易出现性能问题的大坑。网上有很多相关文章,列举了一些导致逃逸情况,其实总结起来就一句话: 多级间接赋值容易导致逃逸。 这里的多级间接指的是,对某个引用类对象中的引用类成员进行赋值。Go语言中的引用类数据类型有func,interface,slice,map,chan,Type(指针)。 记住公式Data。FieldValue,如果Data,Field都是引用类的数据类型,则会导致Value逃逸。这里的等号不单单只赋值,也表示参数传递。 根据公式,我们假设一个变量data是以下几种类型,相应的可以得出结论:〔〕interface{}:data〔0〕100会导致100逃逸map〔string〕interface{}:data〔key〕value会导致value逃逸map〔interface{}〕interface{}:data〔key〕value会导致key和value都逃逸map〔string〕〔〕string:data〔key〕〔〕string{hello}会导致切片逃逸map〔string〕int:赋值时int会逃逸〔〕int:data〔0〕i会使i逃逸func(int):data(i)会使i逃逸func(〔〕string):data(〔〕{hello})会使〔〕string{hello}逃逸chan〔〕string:data〔〕string{hello}会使〔〕string{hello}逃逸以此类推,不一一列举了 下面给出一些实际的例子:函数变量 如果变量值是一个函数,函数的参数又是引用类型,则传递给它的参数都会逃逸。functest(iint){}functestEscape(iint){}funcmain(){i,j,m,n:0,0,0,0t,te:test,testEscape函数变量直接调用test(m)不逃逸testEscape(n)不逃逸间接调用t(i)不逃逸te(j)逃逸}。test。go:4:17:testEscapeidoesnotescape。test。go:11:5:jescapestoheap。test。go:11:5:fromte(j)(parametertoindirectcall)at。test。go:11:4。test。go:7:5:movedtoheap:j。test。go:14:13:mainndoesnotescape 上例中te的类型是func(int),属于引用类型,参数int也是引用类型,则调用te(j)形成了为te的参数(成员)int赋值的现象,即te。ij会导致逃逸。代码中其他几种调用都没有形成多级间接赋值情况。 同理,如果函数的参数类型是slice,map或interface{}都会导致参数逃逸。functestSlice(slice〔〕int){}functestMap(mmap〔int〕int){}functestInterface(iinterface{}){}funcmain(){x,y,z:make(〔〕int,1),make(map〔int〕int),100ts,tm,ti:testSlice,testMap,testInterfacets(x)ts。slicex导致x逃逸tm(y)tm。my导致y逃逸ti(z)ti。iz导致z逃逸}。test。go:3:16:testSliceslicedoesnotescape。test。go:4:14:testMapmdoesnotescape。test。go:5:20:testInterfaceidoesnotescape。test。go:8:17:make(〔〕int,1)escapestoheap。test。go:8:17:fromx(assignpair)at。test。go:8:10。test。go:8:17:fromts(x)(parametertoindirectcall)at。test。go:10:4。test。go:8:33:make(map〔int〕int)escapestoheap。test。go:8:33:fromy(assignpair)at。test。go:8:10。test。go:8:33:fromtm(y)(parametertoindirectcall)at。test。go:11:4。test。go:12:4:zescapestoheap。test。go:12:4:fromti(z)(parametertoindirectcall)at。test。go:12:4 匿名函数的调用也是一样的,它本质上也是一个函数变量。有兴趣的可以自己测试一下。间接赋值typeDatastruct{datamap〔int〕intslice〔〕intchchanintinfinterface{}pint}funcmain(){d1:Data{}d1。datamake(map〔int〕int)GOOD:doesnotescaped1。slicemake(〔〕int,4)GOOD:doesnotescaped1。chmake(chanint,4)GOOD:doesnotescaped1。inf3GOOD:doesnotescaped1。pnew(int)GOOD:doesnotescaped2:new(Data)d2是指针变量,下面为该指针变量中的指针成员赋值d2。datamake(map〔int〕int)BAD:escapetoheapd2。slicemake(〔〕int,4)BAD:escapetoheapd2。chmake(chanint,4)BAD:escapetoheapd2。inf3BAD:escapetoheapd2。pnew(int)BAD:escapetoheap}。test。go:20:16:make(map〔int〕int)escapestoheap。test。go:20:16:fromd2。data(stardotequals)at。test。go:20:10。test。go:21:17:make(〔〕int,4)escapestoheap。test。go:21:17:fromd2。slice(stardotequals)at。test。go:21:11。test。go:22:14:make(chanint,4)escapestoheap。test。go:22:14:fromd2。ch(stardotequals)at。test。go:22:8。test。go:23:9:3escapestoheap。test。go:23:9:fromd2。inf(stardotequals)at。test。go:23:9。test。go:24:12:new(int)escapestoheap。test。go:24:12:fromd2。p(stardotequals)at。test。go:24:7。test。go:13:16:mainmake(map〔int〕int)doesnotescape。test。go:14:17:mainmake(〔〕int,4)doesnotescape。test。go:15:14:mainmake(chanint,4)doesnotescape。test。go:16:9:main3doesnotescape。test。go:17:12:mainnew(int)doesnotescape。test。go:19:11:mainnew(Data)doesnotescapeInterface 只要使用了Interface类型(不是interafce{}),那么赋值给它的变量一定会逃逸。因为interfaceVariable。Method()先是间接的定位到它的实际值,再调用实际值的同名方法,执行时实际值作为参数传递给方法。相当于interfaceVariable。Method。thisrealValuetypeIfaceinterface{Dummy()}typeIntegerintfunc(iInteger)Dummy(){}funcmain(){var(ifaceIfaceiInteger)ifaceiiface。Dummy()makeiescapetoheap形成iface。Dummy。ii}引用类型的channel 向channel中发送数据,本质上就是为channel内部的成员赋值,就像给一个slice中的某一项赋值一样。所以chanType,chanmap〔Type〕Type,chan〔〕Type,chaninterface{}类型都会导致发送到channel中的数据逃逸。 这本来也是情理之中的,发送给channel的数据是要与其他函数分享的,为了保证发送过去的指针依然可用,只能使用堆分配。functest(){var(chIntegermake(chanint)chMapmake(chanmap〔int〕int)chSlicemake(chan〔〕int)chInterfacemake(chaninterface{})a,b,c,d0,map〔int〕int{},〔〕int{},32)chIntegera逃逸chMapb逃逸chSlicec逃逸chInterfaced逃逸}。escape。go:11:15:aescapestoheap。escape。go:11:15:fromchIntegera(send)at。escape。go:11:12。escape。go:9:3:movedtoheap:a。escape。go:9:31:map〔int〕intliteralescapestoheap。escape。go:9:31:fromb(assigned)at。escape。go:9:3。escape。go:9:31:fromchMapb(send)at。escape。go:12:8。escape。go:9:40:〔〕intliteralescapestoheap。escape。go:9:40:fromc(assigned)at。escape。go:9:3。escape。go:9:40:fromchSlicec(send)at。escape。go:13:10。escape。go:14:14:descapestoheap。escape。go:14:14:fromchInterface(interface{})(d)(send)at。escape。go:14:14。escape。go:5:21:testmake(chanint)doesnotescape。escape。go:6:21:testmake(chanmap〔int〕int)doesnotescape。escape。go:7:21:testmake(chan〔〕int)doesnotescape。escape。go:8:21:testmake(chaninterface{})doesnotescape可变参数 可变参数如func(arg。。。string)实际与func(arg〔〕string)是一样的,会增加一层访问路径。这也是fmt。Sprintf总是会使参数逃逸的原因。 例子非常多,这里不能一一列举,我们只需要记住分析方法就好,即,2级或更多级的访问赋值会容易导致数据逃逸。这里加上容易二字是因为随着语言的发展,相信这些问题会被慢慢解决,但现阶段,这个可以作为我们分析逃逸现象的依据。 下面代码中包含2种很常规的写法,但他们却有着很大的性能差距,建议自己想下为什么。typeUserstruct{roles〔〕string}func(uUser)SetRoles(roles〔〕string){u。rolesroles}funcSetRoles(uUser,roles〔〕string)User{u。rolesrolesreturnu} Benchmark和pprof给出的结果:BenchmarkUserSetRoles85000000022。3nsop16Bop1allocsopBenchmarkSetRoles820000000000。51nsop0Bop0allocsop768。01MB768。01MB(flat,cum)100ofTotal。。3:importtesting。。4:。。5:funcBenchmarkUserSetRoles(btesting。B){。。6:u:new(User)。。7:fori:0;ib。N;i{768。01MB768。01MB8:u。SetRoles(〔〕string{admin})看这里。。9:}。。10:}。。11:。。12:funcBenchmarkSetRoles(btesting。B){。。13:fori:0;ib。N;i{ROUTINEtesting。(B)。launchinusrsharegosrctestingbenchmark。go。。。。。。结论 熟悉堆栈概念可以让我们更容易看透Go程序的性能问题,并进行优化。 多级间接赋值会导致Go编译器出现不必要的逃逸,在一些情况下可能我们只需要修改一下数据结构就会使性能有大幅提升。这也是很多人不推荐在Go中使用指针的原因,因为它会增加一级访问路径,而map,slice,interface{}等类型是不可避免要用到的,为了减少不必要的逃逸,只能拿指针开刀了。 大多数情况下,性能优化都会为程序带来一定的复杂度。建议实际项目中还是怎么方便怎么写,功能完成后通过性能分析找到瓶颈所在,再对局部进行优化。