纠纷奇闻作文社交美文家庭
聚热点
家庭城市
爱好生活
创业男女
能力餐饮
美文职业
心理周易
母婴奇趣
两性技能
社交传统
新闻范文
工作个人
思考社会
作文职场
家居中考
兴趣安全
解密魅力
奇闻笑话
写作笔记
阅读企业
饮食时事
纠纷案例
初中历史
说说童话
乐趣治疗

Go语言内存管理(三)逃逸分析

9月25日 逆落雪投稿
  该内容是在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{}等类型是不可避免要用到的,为了减少不必要的逃逸,只能拿指针开刀了。
  大多数情况下,性能优化都会为程序带来一定的复杂度。建议实际项目中还是怎么方便怎么写,功能完成后通过性能分析找到瓶颈所在,再对局部进行优化。
投诉 评论 转载

马斯克发邮件敦促员工提高产量今年目标是交付50万辆IT之家12月13日消息据外媒雅虎报道,特斯拉在写给内部员工的一封邮件遭到曝光,他表示当前对产品的需求超过了生产能力,他称这是一个亟待解决的问题。这封发给员工的邮件还提到……焕彩新声卓越静享森海塞尔HD458BT为出色的无线音质更换新韦德马克,2020年5月28日再一次令人心动的感觉。就像当下热门歌曲被激情演绎,电影的不朽之作被导演重新剪辑,没有什么比对经典作品进行重新解构更让人着迷的了。凭借全新推出的HD……鸿蒙发布在即,用户对鸿蒙的态度,已远远超出华为预期近日,有华为的朋友透露,华为自己都没想到,鸿蒙系统在手机上的应用如此受用户欢迎。从鸿蒙开发,到确定应用在手机端仅用了很短的时间,当时华为内部都非常担心用户不会喜欢升级鸿蒙……使用一个月后,摩米士MagSafe磁吸无线充对于我生活的一些无线充电,其实更像是一个数码玩家的生活仪式感。笔者最先接触磁吸充电其实是在微软SurfacePro笔记本上面有见,第一时间是感觉这个设计真的是很贴心,充电的时候不需要认真……Go语言内存管理(三)逃逸分析该内容是在Go1。6版本的时候写的。Golang经过了多个版本的迭代,逃逸分析机制也在不断的迭代优化,最新版本的效果与文章结论存在一些出入;所以该内容仅供参考。介绍Go语……马斯克特斯拉百万辆自动驾驶出租车计划仍在推进4月13日消息,据外媒报道,电动汽车制造商特斯拉首席执行官埃隆马斯克(ElonMusk)表示,该公司在今年年底前让100万辆自动驾驶出租车上路的计划仍在推进中,目前正在等待监管……中信证券总经理无人驾驶可让滴滴每年节省千亿司机开支8月6日上午消息,在今日的华为5机峰会上,中信证券总经理杨明辉发表演讲。中信证券总经理杨明辉据悉,中信证券从2019年就开始布局5G,目前已经设立了两只5G投资基金……年底换新机,预算一千多还要配置强悍,该怎么选年底想换手机,但是现在好点的手机动辄五六千,实在是不值当的,毕竟好多人买手机需求没有那么高,或者就玩玩游戏,或者就聊天刷视频,那么有没有那种配置高体验好,价格还便宜的手机呢,当……吉利星瑞宣布首个实现燃油车整车OTA可像手机一样持续升级优化IT之家11月22日消息11月20日,第18届广州国际车展正式开幕。从吉利汽车获悉,吉利星瑞发布了首次FOTA升级暨上新计划,宣布成为行业首个实现整车OTA的燃油车。图源……央评阿里真是因为性侵案吗?央评阿里固然有该事件影响大、性质恶劣的因素,更主要的是阿里现在企业文化已经越来越变味了。这次事件从发生到成为热点,阿里各级管理人员相互包庇、推诿扯皮、以大压小,就是想大事化小小……比亚迪否认车型夏存在,新车明已在计划中IT之家1月7日消息据财联社报道,今日,比亚迪相关负责人在明确否认夏存在的同时透露,比亚迪下一款车型将命名为明,但现在还没有明确的时间表。比亚迪汉EV图源:比亚迪官网……比亚迪推2021款唐EV四驱高性能版尊荣型,补贴后28。35IT之家1月6日消息根据比亚迪官方的消息,2021款唐EV加推四驱高性能版尊荣型,补贴后售价28。35万元。IT之家了解到,2020年8月份,2021款唐上市,共推出燃油……
通用汽车继续发力新能源计划2030年将收入翻一番,超越特斯拉芯片短缺难挡市场回暖,上半年全球电动汽车销量达260万辆谁把魅族视为珍品,谁又把魅族视为异类?十大行业方案2020新锐消费品行业智能数据分析解决方案6。98万元起,宝骏KiWiEV今日正式上市预售20天订单量高配宏光MINIEV,宝骏KiWiEV预售一周订单突破300上汽通用五菱年累销量突破百万,宏光MINIEV连续11个月位国内规模最大无锡智能网联汽车开放测试道路群启用哪吒汽车6月销量5138台创新高,同比增长536,哪吒V累计奇瑞QQ冰淇淋6种车身配色曝光,有望于11月上市奇瑞QQ冰淇淋官图发布对标五菱宏光MINIEV,将于成都车展鼠标选购要点有哪些?
如何制作会议人员姓名桌牌三星UA32ES5500支持视频点播吗消防设备联动逻辑关系为心破冰短篇散文许茹芸头超有女人味大班第二学期班级工作计划种下一个回忆作文1000字相机三棵树小学优秀作文吴迎秋MG56。79万元,上汽想干啥?流量的未来新经济推动产业升级支撑就业增长

友情链接:中准网聚热点快百科快传网快生活快软网快好知文好找美丽时装彩妆资讯历史明星乐活安卓数码常识驾车健康苹果问答网络发型电视车载室内电影游戏科学音乐整形