您好,登錄后才能下訂單哦!
這篇文章主要講解了“如何理解Go語言中的逃逸分析”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“如何理解Go語言中的逃逸分析”吧!
1、逃逸分析介紹
2、Go中內存分配在哪里?
3、Go與C++內存分配的區別
4、逃逸分析騷操作
5、逃逸分析引申示例說明
學計算機的同學都知道,在編譯原理中,分析指針動態范圍的方法稱之為逃逸分析。通俗來講,當一個對象的指針被多個方法或線程引用時,我們稱這個指針發生了“逃逸”。
Go語言的逃逸分析是編譯器執行靜態代碼分析后,對內存管理進行的優化和簡化,它可以決定一個變量是分配到堆還棧上。
寫過C/C++的小伙伴應該知道,使用比較經典的malloc
和new
函數可以在堆上分配一塊內存,這塊內存的使用和回收(銷毀)的任務在程序員中,處理不當,很可能會發生內存泄露。
但是在Go語言中,基本不用擔心內存泄露的問題,因為內存回收Go語言中已經幫我們處理了(GC回收機制)。雖然也有new函數,但是使用new
函數得到的內存不一定就在堆上。堆和棧的區別對程序員“模糊化”了,當然這一切都是Go編譯器在背后幫我們完成的。
Go語言逃逸分析最基本的原則是:如果一個函數返回對一個變量的引用,那么它就會發生逃逸。
簡單來說,編譯器會分析代碼的特征和代碼生命周期,Go中的變量只有在編譯器可以證明在函數返回后不會再被引用的,才分配到棧上,其他情況下都是分配到堆上。
Go語言里沒有一個關鍵字或者函數可以直接讓變量被編譯器分配到堆上,相反,編譯器通過分析代碼來決定將變量分配到何處。
對一個變量取地址,可能會被分配到堆上。但是編譯器進行逃逸分析后,如果考察到在函數返回后,此變量不會被引用,那么還是會被分配到棧上。
編譯器會根據變量是否被外部引用來決定是否逃逸:
如果在函數外面沒有引用到,則優先放到棧區中;
如果在函數外面存在引用的可能,則就會放到堆區中;
當我們寫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/C++中的動態分配的內存需要我們手動來釋放,這樣會帶來一個問題:有些內存處理不當或回收不及時,導致內存泄露。
但是這樣的好處是:開發人員可以自己管理內存。
Go的垃圾回收,讓堆和棧對程序員保持透明。真正解放了程序員的雙手,讓他們可以專注于業務,“高效”地完成代碼編寫。把那些內存管理的復雜機制交給編譯器,而程序員可以去享受生活。
逃逸分析這種“騷操作”把變量合理地分配到它該去的地方。即使你是用new申請到的內存,如果我發現你竟然在退出函數后沒有用了,那么就把你丟到棧上,畢竟棧上的內存分配比堆上快很多;反之,即使你表面上只是一個普通的變量,但是經過逃逸分析后發現在退出函數之后還有其他地方在引用,那我就把你分配到堆上。
如果變量都分配到堆上,堆不像棧可以自動清理。它會引起Go頻繁地進行垃圾回收,而垃圾回收會占用比較大的系統開銷(占用CPU容量的25%)。
堆和棧相比,堆適合不可預知大小的內存分配。但是為此付出的代價是分配速度較慢,而且會形成內存碎片。棧內存分配則會非常快。棧分配內存只需要兩個CPU指令:“PUSH
”和“RELEASE
”,分配和釋放;而堆分配內存首先需要去找到一塊大小合適的內存塊,之后要通過垃圾回收才能釋放。
通過逃逸分析,可以盡量把那些不需要分配到堆上的變量直接分配到棧上,堆上的變量少了,會減輕分配堆內存的開銷,同時也會減少gc
的壓力,提高程序的運行速度。
引申1:如何查看某個變量是否發生了逃逸?兩種方法:使用go命令,查看逃逸分析結果;反匯編源碼;
比如用這個例子:
package main import "fmt" func foo() *int { t := 3 return &t; } func main() { x := foo() fmt.Println(*x) }
使用go命令:
go build -gcflags '-m -l' main.go
加-l是為了不讓foo
函數被內聯。得到如下輸出:
# 命令行變量 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{})
,編譯期間很難確定其參數的具體類型,也會發生逃逸。
反匯編代碼比較難理解,這里就不講了。
引申2:下面代碼中的變量發生逃逸了嗎?
先來看示例1:
package main type S struct {} func main() { var x S _ = identity(x) } func identity(x S) S { return x }
分析:Go語言函數傳遞都是通過值的,調用函數的時候,直接在棧上copy出一份參數,不存在逃逸。
再來看示例二:
package main type S struct {} func main() { var x S y := &x _ = *identity(y) } func identity(z *S) *S { return z }
分析:identity
函數的輸入直接當成返回值了,因為沒有對z作引用,所以z沒有逃逸。對x的引用也沒有逃出main
函數的作用域,因此x也沒有發生逃逸。
繼續看示例三:
package main type S struct {} func main() { var x S _ = *ref(x) } func ref(z S) *S { return &z }
分析:z是對x的拷貝,ref函數中對z取了引用,所以z不能放在棧上,否則在ref函數之外,通過引用如何找到z,所以z必須要逃逸到堆上。僅管在main函數中,直接丟棄了ref的結果,但是Go的編譯器還沒有那么智能,分析不出來這種情況。而對x從來就沒有取引用,所以x不會發生逃逸。
還有示例四:如果對一個結構體成員賦引用如何?
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 }
分析:refStruct
函數對y取了引用,所以y發生了逃逸。
最后看示例五:
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 }
分析:在main
函數里對i取了引用,并且把它傳給了refStruct
函數,i的引用一直在main
函數的作用域用,因此i沒有發生逃逸。和上一個例子相比,有一點小差別,但是導致的程序效果是不同的:例子4中,i先在main
的棧幀中分配,之后又在refStruct
棧幀中分配,然后又逃逸到堆上,到堆上分配了一次,共3次分配。本例中,i只分配了一次,然后通過引用傳遞。
感謝各位的閱讀,以上就是“如何理解Go語言中的逃逸分析”的內容了,經過本文的學習后,相信大家對如何理解Go語言中的逃逸分析這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。