您好,登錄后才能下訂單哦!
小編給大家分享一下GoLang逃逸分析的機制是什么,相信大部分人都還不怎么了解,因此分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后大有收獲,下面讓我們一起去了解一下吧!
對于手動管理內存的語言,比如 C/C++,調用著名的malloc和new函數可以在堆上分配一塊內存,這塊內存的使用和銷毀的責任都在程序員。一不小心,就會發生內存泄露,搞得膽戰心驚。
但是 Golang 并不是這樣,雖然 Golang 語言里面也有 new。Golang 編譯器決定變量應該分配到什么地方時會進行逃逸分析。使用new函數得到的內存不一定就在堆上。堆和棧的區別對程序員“模糊化”了,當然這一切都是Go編譯器在背后幫我們完成的。一個變量是在堆上分配,還是在棧上分配,是經過編譯器的逃逸分析之后得出的結論。
一、 逃逸分析是什么
wiki定義
In compiler optimization, escape analysis is a method for determining the dynamic scope of pointers - where in the program a pointer can be accessed. It is related to pointer analysis and shape analysis.
When a variable (or an object) is allocated in a subroutine, a pointer to the variable can escape to other threads of execution, or to calling subroutines. If an implementation uses tail call optimization (usually required for functional languages), objects may also be seen as escaping to called subroutines. If a language supports first-class continuations (as do Scheme and Standard ML of New Jersey), portions of the call stack may also escape.
If a subroutine allocates an object and returns a pointer to it, the object can be accessed from undetermined places in the program — the pointer has "escaped". Pointers can also escape if they are stored in global variables or other data structures that, in turn, escape the current procedure.
Escape analysis determines all the places where a pointer can be stored and whether the lifetime of the pointer can be proven to be restricted only to the current procedure and/or threa.
C/C++中,有時為了提高效率,常常將pass-by-value(傳值)“升級”成pass-by-reference,企圖避免構造函數的運行,并且直接返回一個指針。然而這里隱藏了一個很大的坑:在函數內部定義了一個局部變量,然后返回這個局部變量的地址(指針)。這些局部變量是在棧上分配的(靜態內存分配),一旦函數執行完畢,變量占據的內存會被銷毀,任何對這個返回值作的動作(如解引用),都將擾亂程序的運行,甚至導致程序直接崩潰。例如:
int *foo ( void ) { int t = 3; return &t; }
為了避免這個坑,有個更聰明的做法:在函數內部使用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%)。
堆和棧相比,堆適合不可預知大小的內存分配。但是為此付出的代價是分配速度較慢,而且會形成內存碎片。棧內存分配則會非常快。棧分配內存只需要兩個CPU指令:“PUSH”和“RELEASE”,分配和釋放;而堆分配內存首先需要去找到一塊大小合適的內存塊,之后要通過垃圾回收才能釋放。
通過逃逸分析,可以盡量把那些不需要分配到堆上的變量直接分配到棧上,堆上的變量少了,會減輕分配堆內存的開銷,同時也會減少gc的壓力,提高程序的運行速度。
三、 逃逸分析如何完成
Go逃逸分析最基本的原則是:如果一個函數返回對一個變量的引用,那么它就會發生逃逸。
簡單來說,編譯器會分析代碼的特征和代碼生命周期,Go中的變量只有在編譯器可以證明在函數返回后不會再被引用的,才分配到棧上,其他情況下都是分配到堆上。
Go語言里沒有一個關鍵字或者函數可以直接讓變量被編譯器分配到堆上,相反,編譯器通過分析代碼來決定將變量分配到何處。
對一個變量取地址,可能會被分配到堆上。但是編譯器進行逃逸分析后,如果考察到在函數返回后,此變量不會被引用,那么還是會被分配到棧上。
簡單來說,編譯器會根據變量是否被外部引用來決定是否逃逸:
1)如果函數外部沒有引用,則優先放到棧中;
2) 如果函數外部存在引用,則必定放到堆中;
針對第一條,可能放到堆上的情形:定義了一個很大的數組,需要申請的內存過大,超過了棧的存儲能力。
四、 逃逸分析實例
下面是一個簡單的例子。
package main import () func foo() *int { var x int return &x } func bar() int { x := new(int) *x = 1 return *x } func main() {}
開啟逃逸分析日志很簡單,只要在編譯的時候加上-gcflags '-m',但是我們為了不讓編譯時自動內連函數,一般會加-l參數,最終為-gcflags '-m -l',執行如下命令:
$ go build -gcflags '-m -l' main.go # command-line-arguments ./main.go:5:9: &x escapes to heap ./main.go:4:6: moved to heap: x ./main.go:9:10: bar new(int) does not escape
上面代碼中foo() 中的 x 最后在堆上分配,而 bar() 中的 x 最后分配在了棧上。
也可以使用反匯編命令看出變量是否發生逃逸。
$ go tool compile -S main.go
截取部分結果,圖中標記出來的說明foo中x是在堆上分配內存,發生了逃逸。
反匯編命令結果
什么時候逃逸呢? golang.org FAQ 上有一個關于變量分配的問題如下:
Q: How do I know whether a variable is allocated on the heap or the stack?
A: From a correctness standpoint, you don't need to know. Each variable in Go exists as long as there are references to it. The storage location chosen by the implementation is irrelevant to the semantics of the language.
The storage location does have an effect on writing efficient programs. When possible, the Go compilers will allocate variables that are local to a function in that function's stack frame. However, if the compiler cannot prove that the variable is not referenced after the function returns, then the compiler must allocate the variable on the garbage-collected heap to avoid dangling pointer errors. Also, if a local variable is very large, it might make more sense to store it on the heap rather than the stack.
In the current compilers, if a variable has its address taken, that variable is a candidate for allocation on the heap. However, a basic escape analysis recognizes some cases when such variables will not live past the return from the function and can reside on the stack.
關于什么時候逃逸,什么時候不逃逸,我們接下來再看幾個小例子。
1)Example1
package main type S struct{} func main() { var x S y := &x _ = *identity(y) } func identity(z *S) *S { return z }
結果如下:
# command-line-arguments ./main.go:8:22: leaking param: z to result ~r1 level=0 ./main.go:5:7: main &x does not escape
這里的第一行表示z變量是“流式”,因為identity這個函數僅僅輸入一個變量,又將這個變量作為返回輸出,但identity并沒有引用z,所以這個變量沒有逃逸,而x沒有被引用,且生命周期也在mian里,x沒有逃逸,分配在棧上。
2)Example2
package main type S struct{} func main() { var x S _ = *ref(x) } func ref(z S) *S { return &z }
結果如下:
# command-line-arguments ./main.go:8:9: &z escapes to heap ./main.go:7:16: moved to heap: z
這里的z是逃逸了,原因很簡單,go都是值傳遞,ref函數copy了x的值,傳給z,返回z的指針,然后在函數外被引用,說明z這個變量在函數內聲明,可能會被函數外的其他程序訪問。所以z逃逸了,分配在堆上
3)Example3
package main type S struct { M *int } func main() { var i int refStruct(i) } func refStruct(y int) (z S) { z.M = &y return z }
結果如下:
# command-line-arguments ./main.go:10:8: &y escapes to heap ./main.go:9:26: moved to heap: y
看日志的輸出,這里的y是逃逸了,看來在struct里好像并沒有區別,有可能被函數外的程序訪問就會逃逸
4)Example4
package main type S struct { M *int } func main() { var i int refStruct(&i) } func refStruct(y *int) (z S) { z.M = y return z }
結果如下:
# command-line-arguments ./main.go:9:27: leaking param: y to result z level=0 ./main.go:7:12: main &i does not escape
這里的y沒有逃逸,分配在棧上,原因和Example1是一樣的。
5)Example5
package main type S struct { M *int } func main() { var x S var i int ref(&i, &x) } func ref(y *int, z *S) { z.M = y }
結果如下:
# command-line-arguments ./main.go:10:21: leaking param: y ./main.go:10:21: ref z does not escape ./main.go:8:6: &i escapes to heap ./main.go:7:6: moved to heap: i ./main.go:8:10: main &x does not escape
這里的z沒有逃逸,而i卻逃逸了,這是因為go的逃逸分析不知道z和i的關系,逃逸分析不知道參數y是z的一個成員,所以只能把它分配給堆。
以上是“GoLang逃逸分析的機制是什么”這篇文章的所有內容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內容對大家有所幫助,如果還想學習更多知識,歡迎關注億速云行業資訊頻道!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。