Golang之變量去哪兒?

寫過C/C++的同窗都知道,調用著名的malloc和new函數能夠在堆上分配一塊內存,這塊內存的使用和銷燬的責任都在程序員。一不當心,就會發生內存泄露,搞得膽戰心驚。html

切換到Golang後,基本不會擔憂內存泄露了。雖然也有new函數,可是使用new函數獲得的內存不必定就在堆上。堆和棧的區別對程序員「模糊化」了,固然這一切都是Go編譯器在背後幫咱們完成的。git

一個變量是在堆上分配,仍是在棧上分配,是通過編譯器的逃逸分析以後得出的結論。程序員

這篇文章,就將帶領你們一塊兒去探索逃逸分析——變量到底去哪兒,堆仍是棧?github

什麼是逃逸分析

之前寫C/C++代碼時,爲了提升效率,經常將pass-by-value(傳值)「升級」成pass-by-reference,企圖避免構造函數的運行,而且直接返回一個指針。golang

你必定還記得,這裏隱藏了一個很大的坑:在函數內部定義了一個局部變量,而後返回這個局部變量的地址(指針)。這些局部變量是在棧上分配的(靜態內存分配),一旦函數執行完畢,變量佔據的內存會被銷燬,任何對這個返回值做的動做(如解引用),都將擾亂程序的運行,甚至致使程序直接崩潰。好比下面的這段代碼:shell

int *foo ( void ) {   
    int t = 3;
    return &t;
} 
複製代碼

有些同窗可能知道上面這個坑,用了個更聰明的作法:在函數內部使用new函數構造一個變量(動態內存分配),而後返回此變量的地址。由於變量是在堆上建立的,因此函數退出時不會被銷燬。可是,這樣就好了嗎?new出來的對象該在什麼時候何地delete呢?調用者可能會忘記delete或者直接拿返回值傳給其餘函數,以後就不再能delete它了,也就是發生了內存泄露。關於這個坑,你們能夠去看看《Effective C++》條款21,講得很是好!segmentfault

C++是公認的語法最複雜的語言,聽說沒有人能夠徹底掌握C++的語法。而這一切在Go語言中就大不相同了。像上面示例的C++代碼放到Go裏,沒有任何問題。數組

你表面的光鮮,必定是背後有不少人爲你撐起的!Go語言裏就是編譯器的逃逸分析。它是編譯器執行靜態代碼分析後,對內存管理進行的優化和簡化。bash

在編譯原理中,分析指針動態範圍的方法稱之爲逃逸分析。通俗來說,當一個對象的指針被多個方法或線程引用時,咱們稱這個指針發生了逃逸。函數

更簡單來講,逃逸分析決定一個變量是分配在堆上仍是分配在棧上。

爲何要逃逸分析

前面講的C/C++中出現的問題,在Go中做爲一個語言特性被大力推崇。真是C/C++之砒霜Go之蜜糖!

C/C++中動態分配的內存須要咱們手動釋放,致使猿們平時在寫程序時,如履薄冰。這樣作有他的好處:程序員能夠徹底掌控內存。可是缺點也是不少的:常常出現忘記釋放內存,致使內存泄露。因此,不少現代語言都加上了垃圾回收機制。

Go的垃圾回收,讓堆和棧對程序員保持透明。真正解放了程序員的雙手,讓他們能夠專一於業務,「高效」地完成代碼編寫。把那些內存管理的複雜機制交給編譯器,而程序員能夠去享受生活。

逃逸分析這種「騷操做」把變量合理地分配到它該去的地方,「找準本身的位置」。即便你是用new申請到的內存,若是我發現你居然在退出函數後沒有用了,那麼就把你丟到棧上,畢竟棧上的內存分配比堆上快不少;反之,即便你表面上只是一個普通的變量,可是通過逃逸分析後發如今退出函數以後還有其餘地方在引用,那我就把你分配到堆上。真正地作到「按需分配」,提早實現共產主義!

若是變量都分配到堆上,堆不像棧能夠自動清理。它會引發Go頻繁地進行垃圾回收,而垃圾回收會佔用比較大的系統開銷(佔用CPU容量的25%)。

堆和棧相比,堆適合不可預知大小的內存分配。可是爲此付出的代價是分配速度較慢,並且會造成內存碎片。棧內存分配則會很是快。棧分配內存只須要兩個CPU指令:「PUSH」和「RELEASSE」,分配和釋放;而堆分配內存首先須要去找到一塊大小合適的內存塊,以後要經過垃圾回收才能釋放。

經過逃逸分析,能夠儘可能把那些不須要分配到堆上的變量直接分配到棧上,堆上的變量少了,會減輕分配堆內存的開銷,同時也會減小gc的壓力,提升程序的運行速度。

逃逸分析是怎麼完成的

Go逃逸分析最基本的原則是:若是一個函數返回對一個變量的引用,那麼它就會發生逃逸。

簡單來講,編譯器會分析代碼的特徵和代碼生命週期,Go中的變量只有在編譯器能夠證實在函數返回後不會再被引用的,才分配到棧上,其餘狀況下都是分配到堆上。

Go語言裏沒有一個關鍵字或者函數能夠直接讓變量被編譯器分配到堆上,相反,編譯器經過分析代碼來決定將變量分配到何處。

對一個變量取地址,可能會被分配到堆上。可是編譯器進行逃逸分析後,若是考察到在函數返回後,此變量不會被引用,那麼仍是會被分配到棧上。套個取址符,就想騙補助?Too young!

簡單來講,編譯器會根據變量是否被外部引用來決定是否逃逸:

  1. 若是函數外部沒有引用,則優先放到棧中;
  2. 若是函數外部存在引用,則一定放到堆中;

針對第一條,可能放到堆上的情形:定義了一個很大的數組,須要申請的內存過大,超過了棧的存儲能力。

逃逸分析實例

Go提供了相關的命令,能夠查看變量是否發生逃逸。

仍是用上面咱們提到的例子:

package main

import "fmt"

func foo() *int {
	t := 3
	return &t;
}

func main() {
	x := foo()
	fmt.Println(*x)
}
複製代碼

foo函數返回一個局部變量的指針,main函數裏變量x接收它。執行以下命令:

go build -gcflags '-m -l' main.go
複製代碼

-l是爲了避免讓foo函數被內聯。獲得以下輸出:

# command-line-arguments
src/main.go:7:9: &t escapes to heap
src/main.go:6:7: moved to heap: t
src/main.go:12:14: *x escapes to heap
src/main.go:12:13: main ... argument does not escape
複製代碼

foo函數裏的變量t逃逸了,和咱們預想的一致。讓咱們不解的是爲何main函數裏的x也逃逸了?這是由於有些函數參數爲interface類型,好比fmt.Println(a ...interface{}),編譯期間很難肯定其參數的具體類型,也會發生逃逸。

使用反彙編命令也能夠看出變量是否發生逃逸。

go tool compile -S main.go
複製代碼

截取部分結果,圖中標記出來的說明t是在堆上分配內存,發生了逃逸。

反彙編

總結

堆上動態分配內存比棧上靜態分配內存,開銷大不少。

變量分配在棧上須要能在編譯期肯定它的做用域,不然會分配到堆上。

Go編譯器會在編譯期對考察變量的做用域,並做一系列檢查,若是它的做用域在運行期間對編譯器一直是可知的,那麼就會分配到棧上。

簡單來講,編譯器會根據變量是否被外部引用來決定是否逃逸。對於Go程序員來講,編譯器的這些逃逸分析規則不須要掌握,咱們只需經過go build -gcflags '-m'命令來觀察變量逃逸狀況就好了。

不要盲目使用變量的指針做爲函數參數,雖然它會減小複製操做。但其實當參數爲變量自身的時候,複製是在棧上完成的操做,開銷遠比變量逃逸後動態地在堆上分配內存少的多。

最後,儘可能寫出少一些逃逸的代碼,提高程序的運行效率。

QR

參考資料

【逃逸是怎麼發生的?很贊 結尾有不少參考資料】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/about_go/blob/master/heap_stack.md

【逃逸分析的缺陷】https://studygolang.com/articles/12396

【比較好的逃逸分析的例子】http://www.agardner.me/golang/garbage/collection/gc/escape/analysis/2015/10/18/go-escape-analysis.html

相關文章
相關標籤/搜索