潇洒一哥 發表於 2019-7-14 16:23:00

Go 逃逸分析

<h1 class="md-end-block md-heading"><span class="md-plain md-expand">Go 逃逸分析</span></h1>
<h2 class="md-end-block md-heading"><span class="md-plain">堆和栈</span></h2>
<p class="md-end-block md-p"><span class="md-plain">要理解什么是逃逸分析会涉及堆和栈的一些基本知识,如果忘记的同学我们可以简单的回顾一下:</span></p>
<ul class="ul-list" data-mark="-">
<li class="md-list-item">
<p class="md-end-block md-p"><span class="md-plain">堆(Heap):一般来讲是人为手动进行管理,手动申请、分配、释放。堆适合不可预知大小的内存分配,这也意味着为此付出的代价是分配速度较慢,而且会形成内存碎片。</span></p>
</li>
<li class="md-list-item">
<p class="md-end-block md-p"><span class="md-plain">栈(Stack):由编译器进行管理,自动申请、分配、释放。一般不会太大,因此栈的分配和回收速度非常快;我们常见的函数参数(不同平台允许存放的数量不同),局部变量等都会存放在栈上。</span></p>
</li>
</ul>
<p class="md-end-block md-p"><span class="md-plain">栈分配内存只需要两个CPU指令:“PUSH”和“RELEASE”,分配和释放;而堆分配内存首先需要去找到一块大小合适的内存块,之后要通过垃圾回收才能释放。</span></p>
<p class="md-end-block md-p"><span class="md-plain">通俗比喻的说,<span><code>栈</code><span class="md-plain">就如我们去饭馆吃饭,只需要点菜(发出申请)--》吃吃吃(使用内存)--》吃饱就跑剩下的交给饭馆(操作系统自动回收),而<span><code>堆</code><span class="md-plain">就如在家里做饭,大到家,小到买什么菜,每一个环节都需要自己来实现,但是自由度会大很多。</span></span></span></span></span></p>
<p class="md-end-block md-p">&nbsp;</p>
<h2 class="md-end-block md-heading"><span class="md-plain">什么是逃逸分析</span></h2>
<p class="md-end-block md-p"><span class="md-plain">在编译程序优化理论中,逃逸分析是一种确定指针动态范围的方法,简单来说就是分析在程序的哪些地方可以访问到该指针。</span></p>
<p class="md-end-block md-p"><span class="md-plain">再往简单的说,Go是通过在编译器里做逃逸分析(escape analysis)来决定一个对象放栈上还是放堆上,不逃逸的对象放栈上,可能逃逸的放堆上;即我发现<span><code>变量</code><span class="md-plain">在退出函数后没有用了,那么就把丢到栈上,毕竟栈上的内存分配和回收比堆上快很多;反之,函数内的普通变量经过<span><code>逃逸分析</code><span class="md-plain">后,发现在函数退出后<span><code>变量</code><span class="md-plain">还有在其他地方上引用,那就将<span><code>变量</code><span class="md-plain">分配在堆上。做到按需分配(哪里的人民需要我,我就往哪去~~,一个党员的呐喊)。</span></span></span></span></span></span></span></span></span></p>
<p class="md-end-block md-p">&nbsp;</p>
<h2 class="md-end-block md-heading"><span class="md-plain">为何需要逃逸分析</span></h2>
<p class="md-end-block md-p"><span class="md-plain">ok,了解完<span><code>堆</code><span class="md-plain">和<span><code>栈</code><span class="md-plain">各自的优缺点后,我们就可以更好的知道<span><code>逃逸分析</code><span class="md-plain">存在的目的了:</span></span></span></span></span></span></span></p>
<ol class="ol-list" start="">
<li class="md-list-item">
<p class="md-end-block md-p"><span class="md-plain">减少<span><code>gc</code><span class="md-plain">压力,栈上的变量,随着函数退出后系统直接回收,不需要<span><code>gc</code><span class="md-plain">标记后再清除。</span></span></span></span></span></p>
</li>
<li class="md-list-item">
<p class="md-end-block md-p"><span class="md-plain">减少内存碎片的产生。</span></p>
</li>
<li class="md-list-item">
<p class="md-end-block md-p"><span class="md-plain">减轻分配堆内存的开销,提高程序的运行速度。</span></p>
</li>
</ol>
<p class="md-end-block md-p">&nbsp;</p>
<h2 class="md-end-block md-heading"><span class="md-plain">如何确定是否逃逸</span></h2>
<p class="md-end-block md-p"><span class="md-plain">在<span><code>Go</code><span class="md-plain">中通过逃逸分析日志来确定变量是否逃逸,开启逃逸分析日志:</span></span></span></p>
<div class="cnblogs_code">
<pre>go run -gcflags <span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">-m -l</span><span style="color: rgba(128, 0, 0, 1)">'</span> main.go</pre>
</div>
<ul class="ul-list" data-mark="-">
<li class="md-list-item">
<p class="md-end-block md-p"><span><code>-m</code><span class="md-plain"> 会打印出逃逸分析的优化策略,实际上最多总共可以用 4 个 <span><code>-m</code><span class="md-plain">,但是信息量较大,一般用 1 个就可以了。</span></span></span></span></p>
</li>
<li class="md-list-item md-focus-container">
<p class="md-end-block md-p md-focus"><span class="md-expand"><code>-l</code><span class="md-plain"> 会禁用函数内联,在这里禁用掉<span><code>内联</code><span class="md-plain">能更好的观察逃逸情况,减少干扰。</span></span></span></span></p>
</li>
</ul>
<p class="md-end-block md-p">&nbsp;</p>
<h2 class="md-end-block md-heading"><span class="md-plain">逃逸案例</span></h2>
<h3 class="md-end-block md-heading"><span class="md-plain">案例一:取地址发生逃逸</span></h3>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 0, 1)">package main

type UserData </span><span style="color: rgba(0, 0, 255, 1)">struct</span><span style="color: rgba(0, 0, 0, 1)"> {
Name</span><span style="color: rgba(0, 0, 255, 1)">string</span><span style="color: rgba(0, 0, 0, 1)">
}

func main() {
</span><span style="color: rgba(0, 0, 255, 1)">var</span><span style="color: rgba(0, 0, 0, 1)"> info UserData
info.Name </span>= <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">WilburXu</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">
_ </span>=<span style="color: rgba(0, 0, 0, 1)"> GetUserInfo(info)
}

func GetUserInfo(userInfo UserData) </span>*<span style="color: rgba(0, 0, 0, 1)">UserData {
</span><span style="color: rgba(0, 0, 255, 1)">return</span> &amp;<span style="color: rgba(0, 0, 0, 1)">userInfo
}</span></pre>
</div>
<p>&nbsp;</p>
<p class="md-end-block md-p"><span class="md-plain">执行 <span><code>go run -gcflags '-m -l' main.go</code><span class="md-plain"> 后返回以下结果:</span></span></span></p>
<div class="cnblogs_code">
<pre># command-line-<span style="color: rgba(0, 0, 0, 1)">arguments
.\main.go:</span><span style="color: rgba(128, 0, 128, 1)">14</span>:<span style="color: rgba(128, 0, 128, 1)">9</span>: &amp;<span style="color: rgba(0, 0, 0, 1)">userInfo escapes to heap
.\main.go:</span><span style="color: rgba(128, 0, 128, 1)">13</span>:<span style="color: rgba(128, 0, 128, 1)">18</span>: moved to heap: userInfo</pre>
</div>
<blockquote>
<p class="md-end-block md-p"><span class="md-plain">GetUserInfo函数里面的变量 <span><code>userInfo</code><span class="md-plain"> 逃到堆上了(分配到堆内存空间上了)。</span></span></span></p>
<p class="md-end-block md-p"><span class="md-plain">GetUserInfo 函数的返回值为 *UserData 指针类型,然后 将值变量<span><code>userInfo</code><span class="md-plain"> 的地址返回,此时编译器会判断该值可能会在函数外使用,就将其分配到了堆上,所以变量<span><code>userInfo</code><span class="md-plain">就逃逸了。</span></span></span></span></span></p>
</blockquote>
<h4 class="md-end-block md-heading"><span class="md-plain">优化方案</span></h4>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 0, 1)">func main() {
</span><span style="color: rgba(0, 0, 255, 1)">var</span><span style="color: rgba(0, 0, 0, 1)"> info UserData
info.Name </span>= <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">WilburXu</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">
_ </span>= GetUserInfo(&amp;<span style="color: rgba(0, 0, 0, 1)">info)
}

func GetUserInfo(userInfo </span>*UserData) *<span style="color: rgba(0, 0, 0, 1)">UserData {
</span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> userInfo
}</span></pre>
</div>
<p>&nbsp;</p>
<div class="cnblogs_code">
<pre># command-line-<span style="color: rgba(0, 0, 0, 1)">arguments
.\main.go:</span><span style="color: rgba(128, 0, 128, 1)">13</span>:<span style="color: rgba(128, 0, 128, 1)">18</span>: leaking param: userInfo to result ~r1 level=<span style="color: rgba(128, 0, 128, 1)">0</span><span style="color: rgba(0, 0, 0, 1)">
.\main.go:</span><span style="color: rgba(128, 0, 128, 1)">10</span>:<span style="color: rgba(128, 0, 128, 1)">18</span>: main &amp;info does not escape</pre>
</div>
<p class="md-end-block md-p"><span class="md-plain">对一个变量取地址,可能会被分配到堆上。但是编译器进行逃逸分析后,如果发现到在函数返回后,此变量不会被引用,那么还是会被分配到栈上。套个取址符,就想骗补助?</span></p>
<p class="md-end-block md-p"><span class="md-plain">编译器傲娇的说:Too young,Too Cool...!</span></p>
<p class="md-end-block md-p">&nbsp;</p>
<h3 class="md-end-block md-heading"><span class="md-plain">案例二 :未确定类型</span></h3>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 0, 1)">package main

type User </span><span style="color: rgba(0, 0, 255, 1)">struct</span><span style="color: rgba(0, 0, 0, 1)"> {
name </span><span style="color: rgba(0, 0, 255, 1)">interface</span><span style="color: rgba(0, 0, 0, 1)">{}
}

func main() {
name :</span>= <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">WilburXu</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">
MyPrintln(name)
}

func MyPrintln(one </span><span style="color: rgba(0, 0, 255, 1)">interface</span>{}) (n <span style="color: rgba(0, 0, 255, 1)">int</span><span style="color: rgba(0, 0, 0, 1)">, err error) {
</span><span style="color: rgba(0, 0, 255, 1)">var</span> userInfo = <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)">(User)
userInfo.name </span>= one <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 泛型赋值 逃逸咯</span>
<span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)">
}</span></pre>
</div>
<p>&nbsp;</p>
<p class="md-end-block md-p"><span class="md-plain">执行 <span><code>go run -gcflags '-m -l' main.go</code><span class="md-plain"> 后返回以下结果:</span></span></span></p>
<div class="cnblogs_code">
<pre># command-line-<span style="color: rgba(0, 0, 0, 1)">arguments
.</span>/main.go:<span style="color: rgba(128, 0, 128, 1)">12</span>:<span style="color: rgba(128, 0, 128, 1)">16</span><span style="color: rgba(0, 0, 0, 1)">: leaking param: one
.</span>/main.go:<span style="color: rgba(128, 0, 128, 1)">13</span>:<span style="color: rgba(128, 0, 128, 1)">20</span>: MyPrintln <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)">(User) does not escape
.</span>/main.go:<span style="color: rgba(128, 0, 128, 1)">9</span>:<span style="color: rgba(128, 0, 128, 1)">11</span>: name escapes to heap</pre>
</div>
<p>&nbsp;</p>
<p class="md-end-block md-p"><span class="md-plain">这里可能有同学会好奇,<span><code>MyPrintln</code><span class="md-plain">函数内并没有被引用的便利,为什么变了<span><code>name</code><span class="md-plain">会被分配到了<span><code>堆</code><span class="md-plain">上呢?</span></span></span></span></span></span></span></p>
<p class="md-end-block md-p"><span class="md-plain">上一个案例我们知道了,普通的手法想去"骗取补助",聪明灵利的编译器是不会“上当受骗的噢”;但是对于<span><code>interface</code><span class="md-plain">类型,很遗憾,编译器在编译的时候很难知道在函数的调用或者结构体的赋值过程会是怎么类型,因此只能分配到<span><code>堆</code><span class="md-plain">上。</span></span></span></span></span></p>
<h3 class="md-end-block md-heading"><span class="md-plain">优化方案</span></h3>
<p class="md-end-block md-p"><span class="md-plain">将结构体<span><code>User</code><span class="md-plain">的成员<span><code>name</code><span class="md-plain">的类型、函数<span><code>MyPringLn</code><span class="md-plain">参数<span><code>one</code><span class="md-plain">的类型改为 <span><code>string</code><span class="md-plain">,将得出:</span></span></span></span></span></span></span></span></span></span></span></p>
<div class="cnblogs_code">
<pre># command-line-<span style="color: rgba(0, 0, 0, 1)">arguments
.</span>/main.go:<span style="color: rgba(128, 0, 128, 1)">12</span>:<span style="color: rgba(128, 0, 128, 1)">16</span><span style="color: rgba(0, 0, 0, 1)">: leaking param: one
.</span>/main.go:<span style="color: rgba(128, 0, 128, 1)">13</span>:<span style="color: rgba(128, 0, 128, 1)">20</span>: MyPrintln <span style="color: rgba(0, 0, 255, 1)">new</span>(User) does not escape</pre>
</div>
<h3 class="md-end-block md-heading"><span class="md-plain">拓展分析</span></h3>
<p class="md-end-block md-p"><span class="md-plain">对于案例二的分析,我们还可以通过反编译命令<span><code>go tool compile -S main.go</code><span class="md-plain">查看,会发现如果为<span><code>interface</code><span class="md-plain">类型,main主函数在编译后会<span><code>额外</code><span class="md-plain">多出以下指令:</span></span></span></span></span></span></span></p>
<div class="cnblogs_code">
<pre># main.go:<span style="color: rgba(128, 0, 128, 1)">9</span> -&gt;<span style="color: rgba(0, 0, 0, 1)"> MyPrintln(name)
</span><span style="color: rgba(128, 0, 128, 1)">0x001d</span> <span style="color: rgba(128, 0, 128, 1)">00029</span> (main.go:<span style="color: rgba(128, 0, 128, 1)">9</span>)PCDATA$<span style="color: rgba(128, 0, 128, 1)">2</span>, $<span style="color: rgba(128, 0, 128, 1)">1</span>
<span style="color: rgba(128, 0, 128, 1)">0x001d</span> <span style="color: rgba(128, 0, 128, 1)">00029</span> (main.go:<span style="color: rgba(128, 0, 128, 1)">9</span>)PCDATA$<span style="color: rgba(128, 0, 128, 1)">0</span>, $<span style="color: rgba(128, 0, 128, 1)">1</span>
<span style="color: rgba(128, 0, 128, 1)">0x001d</span> <span style="color: rgba(128, 0, 128, 1)">00029</span> (main.go:<span style="color: rgba(128, 0, 128, 1)">9</span>)LEAQgo.<span style="color: rgba(0, 0, 255, 1)">string</span>.<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">WilburXu</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">(SB), AX
</span><span style="color: rgba(128, 0, 128, 1)">0x0024</span> <span style="color: rgba(128, 0, 128, 1)">00036</span> (main.go:<span style="color: rgba(128, 0, 128, 1)">9</span>)PCDATA$<span style="color: rgba(128, 0, 128, 1)">2</span>, $<span style="color: rgba(128, 0, 128, 1)">0</span>
<span style="color: rgba(128, 0, 128, 1)">0x0024</span> <span style="color: rgba(128, 0, 128, 1)">00036</span> (main.go:<span style="color: rgba(128, 0, 128, 1)">9</span>)MOVQAX, <span style="color: rgba(128, 0, 0, 1)">""</span>..autotmp_5+<span style="color: rgba(128, 0, 128, 1)">32</span><span style="color: rgba(0, 0, 0, 1)">(SP)
</span><span style="color: rgba(128, 0, 128, 1)">0x0029</span> <span style="color: rgba(128, 0, 128, 1)">00041</span> (main.go:<span style="color: rgba(128, 0, 128, 1)">9</span>)MOVQ$<span style="color: rgba(128, 0, 128, 1)">8</span>, <span style="color: rgba(128, 0, 0, 1)">""</span>..autotmp_5+<span style="color: rgba(128, 0, 128, 1)">40</span><span style="color: rgba(0, 0, 0, 1)">(SP)
</span><span style="color: rgba(128, 0, 128, 1)">0x0032</span> <span style="color: rgba(128, 0, 128, 1)">00050</span> (main.go:<span style="color: rgba(128, 0, 128, 1)">9</span>)PCDATA$<span style="color: rgba(128, 0, 128, 1)">2</span>, $<span style="color: rgba(128, 0, 128, 1)">1</span>
<span style="color: rgba(128, 0, 128, 1)">0x0032</span> <span style="color: rgba(128, 0, 128, 1)">00050</span> (main.go:<span style="color: rgba(128, 0, 128, 1)">9</span>)LEAQtype.<span style="color: rgba(0, 0, 255, 1)">string</span><span style="color: rgba(0, 0, 0, 1)">(SB), AX
</span><span style="color: rgba(128, 0, 128, 1)">0x0039</span> <span style="color: rgba(128, 0, 128, 1)">00057</span> (main.go:<span style="color: rgba(128, 0, 128, 1)">9</span>)PCDATA$<span style="color: rgba(128, 0, 128, 1)">2</span>, $<span style="color: rgba(128, 0, 128, 1)">0</span>
<span style="color: rgba(128, 0, 128, 1)">0x0039</span> <span style="color: rgba(128, 0, 128, 1)">00057</span> (main.go:<span style="color: rgba(128, 0, 128, 1)">9</span><span style="color: rgba(0, 0, 0, 1)">)MOVQAX, (SP)
</span><span style="color: rgba(128, 0, 128, 1)">0x003d</span> <span style="color: rgba(128, 0, 128, 1)">00061</span> (main.go:<span style="color: rgba(128, 0, 128, 1)">9</span>)PCDATA$<span style="color: rgba(128, 0, 128, 1)">2</span>, $<span style="color: rgba(128, 0, 128, 1)">1</span>
<span style="color: rgba(128, 0, 128, 1)">0x003d</span> <span style="color: rgba(128, 0, 128, 1)">00061</span> (main.go:<span style="color: rgba(128, 0, 128, 1)">9</span>)LEAQ<span style="color: rgba(128, 0, 0, 1)">""</span>..autotmp_5+<span style="color: rgba(128, 0, 128, 1)">32</span><span style="color: rgba(0, 0, 0, 1)">(SP), AX
</span><span style="color: rgba(128, 0, 128, 1)">0x0042</span> <span style="color: rgba(128, 0, 128, 1)">00066</span> (main.go:<span style="color: rgba(128, 0, 128, 1)">9</span>)PCDATA$<span style="color: rgba(128, 0, 128, 1)">2</span>, $<span style="color: rgba(128, 0, 128, 1)">0</span>
<span style="color: rgba(128, 0, 128, 1)">0x0042</span> <span style="color: rgba(128, 0, 128, 1)">00066</span> (main.go:<span style="color: rgba(128, 0, 128, 1)">9</span>)MOVQAX, <span style="color: rgba(128, 0, 128, 1)">8</span><span style="color: rgba(0, 0, 0, 1)">(SP)
</span><span style="color: rgba(128, 0, 128, 1)">0x0047</span> <span style="color: rgba(128, 0, 128, 1)">00071</span> (main.go:<span style="color: rgba(128, 0, 128, 1)">9</span>)CALLruntime.convT2Estring(SB)</pre>
</div>
<p class="md-end-block md-p"><span class="md-plain">对于<span><code>Go汇编语法</code><span class="md-plain">不熟悉的可以参考 <span class=" md-link"><span class="md-plain">Golang汇编快速指南</span></span></span></span></span></p>
<p class="md-end-block md-p">&nbsp;</p>
<h2 class="md-end-block md-heading"><span class="md-plain">总结</span></h2>
<p class="md-end-block md-p"><span class="md-plain">不要盲目使用变量的指针作为函数参数,虽然它会减少复制操作。但其实当参数为变量自身的时候,复制是在栈上完成的操作,开销远比变量逃逸后动态地在堆上分配内存少的多。</span></p>
<p class="md-end-block md-p"><span class="md-plain">Go的编译器就如一个聪明的<span><code>孩子</code><span class="md-plain">一般,大多时候在逃逸分析问题上的处理都令人眼前一亮,但有时<span><code>闹性子</code><span class="md-plain">的时候处理也是非常粗糙的分析或完全放弃,毕竟这是孩子天性不是吗? 所以也需要我们在编写代码的时候多多观察,多多留意了。</span></span></span></span></span></p>
<p class="md-end-block md-p">&nbsp;</p>
<h2 class="md-end-block md-heading"><span class="md-plain">参考文章</span></h2>
<p class="md-end-block md-p"><span class=" md-link"><span class="md-plain">Golang escape analysis</span></span></p>
<p><span class=" md-link">&nbsp;</span></p>
<p class="md-end-block md-p">&nbsp;</p>
<p class="md-end-block md-p">&nbsp;</p>
<p class="md-end-block md-p">&nbsp;</p><br><br>
来源:https://www.cnblogs.com/wilburxu/p/11184604.html
頁: [1]
查看完整版本: Go 逃逸分析