golang中變數的逃逸分析
go中逃逸分析是怎麼進行的
- 變數逃逸的基本原則是:如果一個函式返回對一個變數的引用,那麼它就會發生逃逸
- 簡單來說編譯器會分析程式碼的特徵和程式碼的生命週期,go中的變數只有在編譯器可以證明函式返回後不會再被引用的,
才會被分配到棧上,其它情況都分配到堆上 - go語言中沒有一個關鍵字或者函式可以直接讓變數被編譯器分配到堆上,相反,編譯器通過分析程式碼來決定將變數
分配到何處 - 對一個變數取地址,可能會被分配到堆上,但是編譯器經過逃逸分析後,如果發現函式返回後,此變數不會再被引用
那麼還是會被分配到棧上 - 編譯器通過變數是否會被外部引用來決定是否逃逸
- 如果函式外部沒有引用,則優先放入棧中
- 如果函式外部存在引用,則必定放入堆中
- 變數在棧上儲存時,函式結束後變數就銷燬了,變數在堆上儲存時,函式結束後變數不會被銷燬
- 逃逸分析這種操作把變數合理的分配到了它該去的地方,即使是使用new申請的記憶體,如果發現在退出函式時沒有用了
就把你丟到棧上,畢竟棧上的記憶體分配比堆上要快很多,反之,即使你表面上是一個普通的變數,但是經過逃逸分析後發現
退出函式時還有其它地方引用,那麼就把它分配到堆上 - 如果把變數都分配到堆上,堆不像棧可以自動的清理,這回引起go頻繁的進行垃圾回收,而垃圾回收佔用比較大的系統開銷
- 堆和棧相比,堆適合不可預知大小的記憶體分配,但是代價是堆記憶體分配較慢,而且會形成記憶體碎片,棧記憶體分配則會非常快
只需要兩個指令,push release即可分配和釋放,而堆分配記憶體首先需要去找到一塊大小何時的記憶體塊,之後通過垃圾回收才能釋放 - 通過逃逸分析可以儘量把那些不需要分配到堆上的變數直接分配帶棧上,堆上的變數變少了,可以減小堆記憶體分配的開銷
同時減輕gc進行垃圾回收的壓力,提高程式的執行速度
【引申1】如何檢視某個變數是否發生了逃逸?
兩種方法:使用go命令,檢視逃逸分析結果;反彙編原始碼;比如用這個例子:
func foo() *int {
t := 3
return &t
}
func main() {
x := foo()
fmt.Println(*x)
}
使用命令:go build -gcflags '-m -l' .\server\main.go
加-l是為了不讓foo函式被內聯。得到如下輸出:
# command-line-arguments
server\main.go:6:2: moved to heap: t
server\main.go:12:13: ... argument does not escape
server\main.go:12:14: *x escapes to heap
foo函式裡的變數t逃逸了,和我們預想的一樣,但是為什麼main函式中的*x也逃逸了,因為有些函式引數是interface型別
編譯期間很難確定它的型別,也會發生逃逸
【引申2】下面程式碼發生逃逸了嗎,為什麼
type S struct {}
func main() {
var s S
_ = identity(s)
}
func identity(x S) S{
return x
}
沒有發生逃逸,因為go語言的函式傳參都是值傳遞,呼叫函式的時候,直接在棧上copy出一份引數,不存在逃逸
type S struct {}
func main() {
var x S
y := &x
_ = *identity(y)
}
func identity(z *S) *S{
return z
}
identity函式的輸入直接當成返回值了,因為沒有對其做引用,所以z沒有逃逸,對x的引用也沒有逃出main函式的作用域,所以x也沒有逃逸
type S struct {}
func main() {
var x S
_ = *identity(x)
}
func identity(z S) *S{
return &z
}
分析,z是對x的拷貝,identity函式中對z取了引用,所以z不能放到棧上,否則在identity函式之外,通過引用如何找到z,
所以z必須逃逸到堆上,儘管在main函式中直接丟棄了identity返回的結果,但是go編譯器還沒有那麼只能,分析不出這種情況
而對x從來沒有取引用,所以x不會發生逃逸
【引申4】如果對一個結構體成員賦引用如何
type S struct {
M *int
}
func main() {
var x = 5
_ = refStruct(x)
}
func refStruct(y int) (z S) {
z.M = &y
return z
}
refStruct函式對y取了引用,所以y發生了逃逸
【示例5】
type S struct {
M *int
}
func main() {
var x = 5
_ = refStruct(&x)
}
func refStruct(y *int) (z S) {
z.M = y
return z
}
分析:在main函式裡對i取了引用,並且把它傳給了refStruct函式,i的引用一直在main函式的作用域用,
因此i沒有發生逃逸。和上一個例子相比,有一點小差別,但是導致的程式效果是不同的:例子4中,i先在main的棧幀中分配,
之後又在refStruct棧幀中分配,然後又逃逸到堆上,到堆上分配了一次,共3次分配。本例中,i只分配了一次,然後通過引用傳遞。
【示例6】
type S struct {
M *int
}
func main() {
var x = 5
var s S
refStruct(&x, &s)
}
func refStruct(y *int, z *S) {
z.M = y
}
分析:本例i發生了逃逸,按照前面例子5的分析,i不會逃逸。兩個例子的區別是例子5中的S是在返回值裡的,
輸入只能“流入”到輸出,本例中的S是在輸入引數中,所以逃逸分析失敗,i要逃逸到堆上。