1. 前言
所謂的逃逸分析(Escape analysis)是指由編譯器決定記憶體分配的位置嗎不需要程式員指定,
函式中申請一個新的物件
- 如果分配在堆疊中, 則函式執行結束后可自動將記憶體回收
- 如果分配在堆中, 則函式執行借宿可交給GC(垃圾回收)處理
有了逃逸分析,回傳函式區域變數將變得可能,除此之外,逃逸分析還跟閉包息息相關,了解哪些場景下物件會逃逸至關重要,
2. 逃逸策略
每當函式中申請新的物件,編譯器會根據該物件是否被函式外部參考來決定是否逃逸:
- 如果函式外部沒有參考,則優先放到堆疊中;
- 如果函式外部存在參考,則必定放到堆中;
注意,對于函式外部沒有參考的物件,也有可能放到堆中,比如記憶體過大超過堆疊的存盤能力,
3. 逃逸場景
3.1 指標逃逸
我們知道Go可以回傳區域變數指標,這其實是一個典型的變數逃逸案例,示例代碼如下:
package main
type Student struct {
Name string
Age int
}
func StudentRegister(name string, age int) *Student {
s := new(Student) //區域變數s逃逸到堆
s.Name = name
s.Age = age
return s
}
func main() {
StudentRegister("Jim", 18)
}
函式StudentRegister()內部s為區域變數,其值通過函式回傳值回傳,s本身為一指標,其指向的記憶體地址不會是堆疊而是堆,這就是典型的逃逸案例,
通過編譯引數-gcflag=-m可以查看編譯程序中的逃逸分析:
D:\SourceCode\GoExpert\src>go build -gcflags=-m
# _/D_/SourceCode/GoExpert/src
.\main.go:8: can inline StudentRegister
.\main.go:17: can inline main
.\main.go:18: inlining call to StudentRegister
.\main.go:8: leaking param: name
.\main.go:9: new(Student) escapes to heap
.\main.go:18: main new(Student) does not escape
可見在StudentRegister()函式中,也即代碼第9行顯示”escapes to heap”,代表該行記憶體分配發生了逃逸現象,
3.2 堆疊空間不足逃逸
看下面的代碼,是否會產生逃逸呢?
package main
func Slice() {
s := make([]int, 1000, 1000)
for index, _ := range s {
s[index] = index
}
}
func main() {
Slice()
}
上面代碼Slice()函式中分配了一個1000個長度的切片,是否逃逸取決于堆疊空間是否足夠大,
直接查看編譯提示,如下:
D:\SourceCode\GoExpert\src>go build -gcflags=-m
# _/D_/SourceCode/GoExpert/src
.\main.go:4: Slice make([]int, 1000, 1000) does not escape
我們發現此處并沒有發生逃逸,那么把切片長度擴大10倍即10000會如何呢?
D:\SourceCode\GoExpert\src>go build -gcflags=-m
# _/D_/SourceCode/GoExpert/src
.\main.go:4: make([]int, 10000, 10000) escapes to heap
我們發現當切片長度擴大到10000時就會逃逸,
實際上當堆疊空間不足以存放當前物件時或無法判斷當前切片長度時會將物件分配到堆中,
3.3 動態型別逃逸
很多函式引數為interface型別,比如fmt.Println(a …interface{}),編譯期間很難確定其引數的具體型別,也會產生逃逸,
如下代碼所示:
package main
import "fmt"
func main() {
s := "Escape"
fmt.Println(s)
}
上述代碼s變數只是一個string型別變數,呼叫fmt.Println()時會產生逃逸:
D:\SourceCode\GoExpert\src>go build -gcflags=-m
# _/D_/SourceCode/GoExpert/src
.\main.go:7: s escapes to heap
.\main.go:7: main ... argument does not escape
3.4 閉包參考物件逃逸
某著名的開源框架實作了某個回傳Fibonacci數列的函式:
func Fibonacci() func() int {
a, b := 0, 1
return func() int {
a, b = b, a+b
return a
}
}
該函式回傳一個閉包,閉包參考了函式的區域變數a和b,使用時通過該函式獲取該閉包,然后每次執行閉包都會依次輸出Fibonacci數列,
完整的示例程式如下所示:
package main
import "fmt"
func Fibonacci() func() int {
a, b := 0, 1
return func() int {
a, b = b, a+b
return a
}
}
func main() {
f := Fibonacci()
for i := 0; i < 10; i++ {
fmt.Printf("Fibonacci: %d\n", f())
}
}
上述代碼通過Fibonacci()獲取一個閉包,每次執行閉包就會列印一個Fibonacci數值,輸出如下所示:
D:\SourceCode\GoExpert\src>src.exe
Fibonacci: 1
Fibonacci: 1
Fibonacci: 2
Fibonacci: 3
Fibonacci: 5
Fibonacci: 8
Fibonacci: 13
Fibonacci: 21
Fibonacci: 34
Fibonacci: 55
Fibonacci()函式中原本屬于區域變數的a和b由于閉包的參考,不得不將二者放到堆上,以致產生逃逸:
D:\SourceCode\GoExpert\src>go build -gcflags=-m
# _/D_/SourceCode/GoExpert/src
.\main.go:7: can inline Fibonacci.func1
.\main.go:7: func literal escapes to heap
.\main.go:7: func literal escapes to heap
.\main.go:8: &a escapes to heap
.\main.go:6: moved to heap: a
.\main.go:8: &b escapes to heap
.\main.go:6: moved to heap: b
.\main.go:17: f() escapes to heap
.\main.go:17: main ... argument does not escape
4 逃逸總結
- 堆疊上分配記憶體比在堆中分配記憶體有更高的效率
- 堆疊上分配的記憶體不需要GC處理
- 堆上分配的記憶體使用完畢會交給GC處理
- 逃逸分析目的是決定內分配地址是堆疊還是堆
- 逃逸分析在編譯階段完成
5. 注意事項
?永遠年輕,永遠熱淚盈眶?思考一下這個問題:函式傳遞指標真的比傳值效率高嗎?
我們知道傳遞指標可以減少底層值的拷貝,可以提高效率,但是如果拷貝的資料量小,由于指標傳遞會產生逃逸,可能會使用堆,也可能會增加GC的負擔,所以傳遞指標不一定是高效的,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/546906.html
標籤:Go
