Golang之变量去哪儿
寫過C/C++的同學都知道,調用著名的malloc和new函數可以在堆上分配一塊內存,這塊內存的使用和銷毀的責任都在程序員。一不小心,就會發生內存泄露,搞得膽戰心驚。
切換到Golang后,基本不會擔心內存泄露了。雖然也有new函數,但是使用new函數得到的內存不一定就在堆上。堆和棧的區別對程序員“模糊化”了,當然這一切都是Go編譯器在背后幫我們完成的。
一個變量是在堆上分配,還是在棧上分配,是經過編譯器的逃逸分析之后得出的結論。
這篇文章,就將帶領大家一起去探索逃逸分析——變量到底去哪兒,堆還是棧?
以前寫C/C++代碼時,為了提高效率,常常將pass-by-value(傳值)“升級”成pass-by-reference,企圖避免構造函數的運行,并且直接返回一個指針。
你一定還記得,這里隱藏了一個很大的坑:在函數內部定義了一個局部變量,然后返回這個局部變量的地址(指針)。這些局部變量是在棧上分配的(靜態內存分配),一旦函數執行完畢,變量占據的內存會被銷毀,任何對這個返回值作的動作(如解引用),都將擾亂程序的運行,甚至導致程序直接崩潰。比如下面的這段代碼:
有些同學可能知道上面這個坑,用了個更聰明的做法:在函數內部使用new函數構造一個變量(動態內存分配),然后返回此變量的地址。因為變量是在堆上創建的,所以函數退出時不會被銷毀。但是,這樣就行了嗎?new出來的對象該在何時何地delete呢?調用者可能會忘記delete或者直接拿返回值傳給其他函數,之后就再也不能delete它了,也就是發生了內存泄露。關于這個坑,大家可以去看看《Effective C++》條款21,講得非常好!
C++是公認的語法最復雜的語言,據說沒有人可以完全掌握C++的語法。而這一切在Go語言中就大不相同了。像上面示例的C++代碼放到Go里,沒有任何問題。
你表面的光鮮,一定是背后有很多人為你撐起的!Go語言里就是編譯器的逃逸分析。它是編譯器執行靜態代碼分析后,對內存管理進行的優化和簡化。
在編譯原理中,分析指針動態范圍的方法稱之為逃逸分析。通俗來講,當一個對象的指針被多個方法或線程引用時,我們稱這個指針發生了逃逸。
更簡單來說,逃逸分析決定一個變量是分配在堆上還是分配在棧上。
前面講的C/C++中出現的問題,在Go中作為一個語言特性被大力推崇。真是C/C++之砒霜Go之蜜糖!
C/C++中動態分配的內存需要我們手動釋放,導致猿們平時在寫程序時,如履薄冰。這樣做有他的好處:程序員可以完全掌控內存。但是缺點也是很多的:經常出現忘記釋放內存,導致內存泄露。所以,很多現代語言都加上了垃圾回收機制。
Go的垃圾回收,讓堆和棧對程序員保持透明。真正解放了程序員的雙手,讓他們可以專注于業務,“高效”地完成代碼編寫。把那些內存管理的復雜機制交給編譯器,而程序員可以去享受生活。
逃逸分析這種“騷操作”把變量合理地分配到它該去的地方,“找準自己的位置”。即使你是用new申請到的內存,如果我發現你竟然在退出函數后沒有用了,那么就把你丟到棧上,畢竟棧上的內存分配比堆上快很多;反之,即使你表面上只是一個普通的變量,但是經過逃逸分析后發現在退出函數之后還有其他地方在引用,那我就把你分配到堆上。真正地做到“按需分配”,提前實現共產主義!
如果變量都分配到堆上,堆不像棧可以自動清理。它會引起Go頻繁地進行垃圾回收,而垃圾回收會占用比較大的系統開銷(占用CPU容量的25%)。
堆和棧相比,堆適合不可預知大小的內存分配。但是為此付出的代價是分配速度較慢,而且會形成內存碎片。棧內存分配則會非??臁7峙鋬却嬷恍枰獌蓚€CPU指令:“PUSH”和“RELEASSE”,分配和釋放;而堆分配內存首先需要去找到一塊大小合適的內存塊,之后要通過垃圾回收才能釋放。
通過逃逸分析,可以盡量把那些不需要分配到堆上的變量直接分配到棧上,堆上的變量少了,會減輕分配堆內存的開銷,同時也會減少gc的壓力,提高程序的運行速度。
Go逃逸分析最基本的原則是:如果一個函數返回對一個變量的引用,那么它就會發生逃逸。
簡單來說,編譯器會分析代碼的特征和代碼生命周期,Go中的變量只有在編譯器可以證明在函數返回后不會再被引用的,才分配到棧上,其他情況下都是分配到堆上。
Go語言里沒有一個關鍵字或者函數可以直接讓變量被編譯器分配到堆上,相反,編譯器通過分析代碼來決定將變量分配到何處。
對一個變量取地址,可能會被分配到堆上。但是編譯器進行逃逸分析后,如果考察到在函數返回后,此變量不會被引用,那么還是會被分配到棧上。套個取址符,就想騙補助?Too young!
簡單來說,編譯器會根據變量是否被外部引用來決定是否逃逸:
如果函數外部沒有引用,則優先放到棧中;
如果函數外部存在引用,則必定放到堆中;
針對第一條,可能放到堆上的情形:定義了一個很大的數組,需要申請的內存過大,超過了棧的存儲能力。
Go提供了相關的命令,可以查看變量是否發生逃逸。
還是用上面我們提到的例子:
foo函數返回一個局部變量的指針,main函數里變量x接收它。執行如下命令:
加-l是為了不讓foo函數被內聯。得到如下輸出:
foo函數里的變量t逃逸了,和我們預想的一致。讓我們不解的是為什么main函數里的x也逃逸了?這是因為有些函數參數為interface類型,比如fmt.Println(a ...interface{}),編譯期間很難確定其參數的具體類型,也會發生逃逸。
使用反匯編命令也可以看出變量是否發生逃逸。
截取部分結果,圖中標記出來的說明 t是在堆上分配內存,發生了逃逸。
堆上動態分配內存比棧上靜態分配內存,開銷大很多。
變量分配在棧上需要能在編譯期確定它的作用域,否則會分配到堆上。
Go編譯器會在編譯期對考察變量的作用域,并作一系列檢查,如果它的作用域在運行期間對編譯器一直是可知的,那么就會分配到棧上。
簡單來說,編譯器會根據變量是否被外部引用來決定是否逃逸。對于Go程序員來說,編譯器的這些逃逸分析規則不需要掌握,我們只需通過go build-gcflags'-m'命令來觀察變量逃逸情況就行了。
不要盲目使用變量的指針作為函數參數,雖然它會減少復制操作。但其實當參數為變量自身的時候,復制是在棧上完成的操作,開銷遠比變量逃逸后動態地在堆上分配內存少的多。
最后,盡量寫出少一些逃逸的代碼,提升程序的運行效率。
【逃逸是怎么發生的】https://www.do1618.com/archives/1328/go-%E5%86%85%E5%AD%98%E9%80%83%E9%80%B8%E8%AF%A6%E7%BB%86%E5%88%86%E6%9E%90/
【Go的變量到底在堆還是棧中分配】https://github.com/developer-learning/night-reading-go/blob/master/content/discuss/2018-07-09-make-new-in-go.md
【Golang堆棧的理解】https://segmentfault.com/a/1190000017498101
【逃逸分析 編寫棧分配內存建議】https://segment.com/blog/allocation-efficiency-in-high-performance-go-services/ 【逃逸分析 比較簡潔】https://studygolang.com/articles/17584
【逃逸分析定義】https://cloud.tencent.com/developer/article/1117410
【逃逸分析例子】https://my.oschina.net/renhc/blog/2222104
https://gocn.vip/article/355 【匯編代碼 傳參】https://github.com/maniafish/aboutgo/blob/master/heapstack.md
【逃逸分析的缺陷】https://studygolang.com/articles/12396
【比較好的逃逸分析的例子】http://www.agardner.me/golang/garbage/collection/gc/escape/analysis/2015/10/18/go-escape-analysis.html
總結
以上是生活随笔為你收集整理的Golang之变量去哪儿的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【原创】“三次握手,四次挥手”你真的懂吗
- 下一篇: 图解Go语言内存分配