Go 逃逸分析

原文地址:Go 逃逸分析html

什麼是逃逸分析

堆和棧

要理解什麼是逃逸分析會涉及堆和棧的一些基本知識,若是忘記的同窗咱們能夠簡單的回顧一下:git

  • 堆(Heap):通常來說是人爲手動進行管理,手動申請、分配、釋放。堆適合不可預知大小的內存分配,這也意味着爲此付出的代價是分配速度較慢,並且會造成內存碎片。
  • 棧(Stack):由編譯器進行管理,自動申請、分配、釋放。通常不會太大,所以棧的分配和回收速度很是快;咱們常見的函數參數(不一樣平臺容許存放的數量不一樣),局部變量等都會存放在棧上。

棧分配內存只須要兩個CPU指令:「PUSH」和「RELEASE」,分配和釋放;而堆分配內存首先須要去找到一塊大小合適的內存塊,以後要經過垃圾回收才能釋放。github

通俗比喻的說,就如咱們去飯館吃飯,只須要點菜(發出申請)--》吃吃吃(使用內存)--》吃飽就跑剩下的交給飯館(操做系統自動回收),而就如在家裏作飯,大到家,小到買什麼菜,每個環節都須要本身來實現,可是自由度會大不少。
在編譯程序優化理論中,逃逸分析是一種肯定指針動態範圍的方法,簡單來講就是分析在程序的哪些地方能夠訪問到該指針。golang

逃逸分析

再往簡單的說,Go是經過在編譯器裏作逃逸分析(escape analysis)來決定一個對象放棧上仍是放堆上,不逃逸的對象放棧上,可能逃逸的放堆上;即我發現變量在退出函數後沒有用了,那麼就把丟到棧上,畢竟棧上的內存分配和回收比堆上快不少;反之,函數內的普通變量通過逃逸分析後,發如今函數退出後變量還有在其餘地方上引用,那就將變量分配在堆上。作到按需分配(哪裏的人民須要我,我就往哪去~~,一個黨員的吶喊)。shell

爲什麼須要逃逸分析

ok,瞭解完各自的優缺點後,咱們就能夠更好的知道逃逸分析存在的目的了:segmentfault

  1. 減小gc壓力,棧上的變量,隨着函數退出後系統直接回收,不須要gc標記後再清除。
  2. 減小內存碎片的產生。
  3. 減輕分配堆內存的開銷,提升程序的運行速度。

如何肯定是否逃逸

Go中經過逃逸分析日誌來肯定變量是否逃逸,開啓逃逸分析日誌:函數

go run -gcflags '-m -l' main.go
  • -m 會打印出逃逸分析的優化策略,實際上最多總共能夠用 4 個 -m,可是信息量較大,通常用 1 個就能夠了。
  • -l 會禁用函數內聯,在這裏禁用掉內聯能更好的觀察逃逸狀況,減小干擾。

逃逸案例

案例一:取地址發生逃逸

package main

type UserData struct {
    Name  string
}

func main() {
    var info UserData
    info.Name = "WilburXu"
    _ = GetUserInfo(info)
}

func GetUserInfo(userInfo UserData) *UserData {
    return &userInfo
}

執行 go run -gcflags '-m -l' main.go 後返回如下結果:優化

# command-line-arguments
.\main.go:14:9: &userInfo escapes to heap
.\main.go:13:18: moved to heap: userInfo
GetUserInfo函數裏面的變量 userInfo 逃到堆上了(分配到堆內存空間上了)。

GetUserInfo 函數的返回值爲 *UserData 指針類型,而後 將值變量userInfo 的地址返回,此時編譯器會判斷該值可能會在函數外使用,就將其分配到了堆上,因此變量userInfo就逃逸了。google

優化方案

func main() {
    var info UserData
    info.Name = "WilburXu"
    _ = GetUserInfo(&info)
}

func GetUserInfo(userInfo *UserData) *UserData {
    return userInfo
}
# command-line-arguments
.\main.go:13:18: leaking param: userInfo to result ~r1 level=0
.\main.go:10:18: main &info does not escape

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

編譯器傲嬌的說:Too young,Too Cool...!

案例二 :未肯定類型

package main

type User struct {
    name interface{}
}

func main() {
    name := "WilburXu"
    MyPrintln(name)
}

func MyPrintln(one interface{}) (n int, err error) {
    var userInfo = new(User)
    userInfo.name = one // 泛型賦值 逃逸咯
    return
}

執行 go run -gcflags '-m -l' main.go 後返回如下結果:

# command-line-arguments
./main.go:12:16: leaking param: one
./main.go:13:20: MyPrintln new(User) does not escape
./main.go:9:11: name escapes to heap

這裏可能有同窗會好奇,MyPrintln函數內並無被引用的便利,爲何變了name會被分配到了上呢?

上一個案例咱們知道了,普通的手法想去"騙取補助",聰明靈利的編譯器是不會「上當受騙的噢」;可是對於interface類型,很遺憾,編譯器在編譯的時候很難知道在函數的調用或者結構體的賦值過程會是怎麼類型,所以只能分配到上。

優化方案

將結構體User的成員name的類型、函數MyPringLn參數one的類型改成 string,將得出:

# command-line-arguments
./main.go:12:16: leaking param: one
./main.go:13:20: MyPrintln new(User) does not escape

拓展分析

對於案例二的分析,咱們還能夠經過反編譯命令go tool compile -S main.go查看,會發現若是爲interface類型,main主函數在編譯後會額外多出如下指令:

# main.go:9 -> MyPrintln(name)
    0x001d 00029 (main.go:9)    PCDATA    $2, $1
    0x001d 00029 (main.go:9)    PCDATA    $0, $1
    0x001d 00029 (main.go:9)    LEAQ    go.string."WilburXu"(SB), AX
    0x0024 00036 (main.go:9)    PCDATA    $2, $0
    0x0024 00036 (main.go:9)    MOVQ    AX, ""..autotmp_5+32(SP)
    0x0029 00041 (main.go:9)    MOVQ    $8, ""..autotmp_5+40(SP)
    0x0032 00050 (main.go:9)    PCDATA    $2, $1
    0x0032 00050 (main.go:9)    LEAQ    type.string(SB), AX
    0x0039 00057 (main.go:9)    PCDATA    $2, $0
    0x0039 00057 (main.go:9)    MOVQ    AX, (SP)
    0x003d 00061 (main.go:9)    PCDATA    $2, $1
    0x003d 00061 (main.go:9)    LEAQ    ""..autotmp_5+32(SP), AX
    0x0042 00066 (main.go:9)    PCDATA    $2, $0
    0x0042 00066 (main.go:9)    MOVQ    AX, 8(SP)
    0x0047 00071 (main.go:9)    CALL    runtime.convT2Estring(SB)

對於Go彙編語法不熟悉的能夠參考 Golang彙編快速指南

案例三:間接賦值(Assignment to indirection escapes)

對某個引用類對象中的引用類成員進行賦值。Go 語言中的引用類數據類型有 func, interface, slice, map, chan, *Type(指針)

package main

type User struct {
    name interface{}
    age *int
}

func main() {
    var (
        userOne User
        userTwo = new(User)
    )
    userOne.name = "WilburXuOne"    // 不逃逸
    userTwo.name = "WilburXuTwo"    // 逃逸

    userOne.age = new(int)    // 不逃逸
    userTwo.age = new(int)    // 逃逸
}

執行 go run -gcflags '-m -l' main.go 後返回如下結果:

# command-line-arguments
.\main.go:14:17: "WilburXuTwo" escapes to heap
.\main.go:17:19: new(int) escapes to heap
.\main.go:11:16: main new(User) does not escape
.\main.go:13:17: main "WilburXuOne" does not escape
.\main.go:16:19: main new(int) does not escape

爲何這裏類型不會逃逸而引用類型會逃逸呢?這是由於在 userTwo = new(User) 對象的建立時,編譯器先是分析userTwo 對象可能分配在上,同時成員變量 nameage 也爲引用類型,爲了保證不出現回收後,致使對象userTwo的成員值也被回收,因此nameage須要逃逸。

可是,若是nameage爲值類型,那麼編譯器雖然初步分析userTwo會分配在上,但因爲main主函數結束後,變量都會被回收,也就是說對象沒有被其餘引用,那麼就都會分配在上,因此nameage沒有發生逃逸。

優化建議

儘可能不要將引用對象賦值給引用對象

總結

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

Go的編譯器就如一個聰明的孩子通常,大多時候在逃逸分析問題上的處理都使人眼前一亮,但有時鬧性子的時候處理也是很是粗糙的分析或徹底放棄,畢竟這是孩子天性不是嗎? 因此也須要咱們在編寫代碼的時候多多觀察,多多留意了。

參考文章

http://www.agardner.me/golang...

https://segmentfault.com/a/11...

https://docs.google.com/docum...

http://npat-efault.github.io/...

相關文章
相關標籤/搜索