首頁 > 軟體

淺談Golang記憶體逃逸

2022-08-08 14:01:09

1.什麼是記憶體逃逸

在一段程式中,每一個函數都會有自己的記憶體區域分配自己的區域性變數,返回值,這些記憶體會由編譯器在棧中進行分配,每一個函數會分配一個棧幀,在函數執行結束後銷燬,但是有些變數我們想在函數執行結束後仍然使用,就需要把這個變數分配在堆上,這種從“棧”上逃逸到“堆”上的現象叫做記憶體逃逸

2.什麼是逃逸分析

雖然Go語言引入的Gc,GC機制會對堆上的物件進行管理,當某個物件不可達(沒有其他物件參照他),他將會被回收。雖然GC可以降低工作人員負擔,但是GC也會給程式帶來效能損耗,當堆記憶體上有大量的堆記憶體物件,就會給GC很大的壓力,雖然Go語言使用的是標記清除演演算法,並且在此基礎上使用了三色標記法和寫屏障技術,但是我們在堆上分配大量記憶體,仍然會對GC造成很大壓力,Go引入了逃逸分析,就是想減少堆記憶體的分配,可以在棧分配的記憶體儘量分配在棧上

3.小結

逃逸分析就是在程式編譯階段根據程式碼中的資料流,對程式碼中哪些變數需要在棧上分配,哪些需要在物件分配的靜態分析方法,堆和棧相比,堆適合分配不可預知大小的記憶體,但是付出代價是分配速度慢,容易產生碎片,棧分配十分快,棧分配只需要兩個指令“Push”和"Release"分配和釋放,而且堆分配需要先找一塊適合大小的記憶體塊分配,需要垃圾回收釋放,所以逃逸分析可以更好的做記憶體分配

Go語言的逃逸分析

src/cmd/compile/internal/gc/escape.go

  • pointers to stack objects cannot be stored in the heap: 指向棧物件的指標不能儲存在堆中
  • pointers to a stack object cannot outlive that object:指向棧物件的指標不能超過該物件的存活期,指標不能在棧物件銷燬之後依然存活(例子:宣告的函數返回並銷燬了物件的棧幀,或者它在迴圈迭代中被重複用於邏輯上不同的變數)

既然逃逸分析是在編譯階段進行的,那我們就可以通過go build -gcflga '-m -m l'檢視逃逸分析結果

4.逃逸分析案例

1.函數返回區域性指標變數

func Add(x,y int) *int {
 res := 0
 res = x + y
 return &res
}
func main()  {
 Add(1,2)
}

.pointer.go:4:2: res escapes to heap:
.pointer.go:4:2:   flow: ~r2 = &res:
.pointer.go:4:2:     from &res (address-of) at .pointer.go:6:9
.pointer.go:4:2:     from return &res (return) at .pointer.go:6:2
.pointer.go:4:2: moved to heap: res

函數返回區域性變數是一個指標變數,函數Add執行結束,對應棧幀就會銷燬,但是參照返回到函數外部,如果我們外部解析地址,就會導致程式存取非法記憶體,所以經過編輯器分析過後將其在堆上分配

2.interface型別逃逸

1.interface產生逃逸

func main()  {
   str := "荔枝"
   fmt.Println(str)
}

E:GoStudysrcHighBaseEscape>go build -gcflags="-m -m -l" ./pointer.go
# command-line-arguments
.pointer.go:20:13: str escapes to heap:
.pointer.go:20:13:   flow: {storage for ... argument} = &{storage for str}:
.pointer.go:20:13:     from str (spill) at .pointer.go:20:13
.pointer.go:20:13:     from ... argument (slice-literal-element) at .pointer.go:20:13
.pointer.go:20:13:   flow: {heap} = {storage for ... argument}:
.pointer.go:20:13:     from ... argument (spill) at .pointer.go:20:13
.pointer.go:20:13:     from fmt.Println(... argument...) (call parameter) at .pointer.go:20:13
.pointer.go:20:13: ... argument does not escape
.pointer.go:20:13: str escapes to heap

str是main的一個區域性變數,傳給 fmt.Printl()之後逃逸,因為fmt.Println()的入參是interface{}型別,如果引數為interface{},那麼編譯期間就很難確定引數型別

2.指向棧物件的指標不能在堆中

我們把程式碼改成這樣

func main()  {
   str := "蘇珊"
   fmt.Println(&str)
}

# command-line-arguments
.pointer.go:19:2: str escapes to heap:
.pointer.go:19:2:   flow: {storage for ... argument} = &str:
.pointer.go:19:2:     from &str (address-of) at .pointer.go:20:14
.pointer.go:19:2:     from &str (interface-converted) at .pointer.go:20:14
.pointer.go:19:2:     from ... argument (slice-literal-element) at .pointer.go:20:13
.pointer.go:19:2:   flow: {heap} = {storage for ... argument}:
.pointer.go:19:2:     from ... argument (spill) at .pointer.go:20:13
.pointer.go:19:2:     from fmt.Println(... argument...) (call parameter) at .pointer.go:20:13
.pointer.go:19:2: moved to heap: str
.pointer.go:20:13: ... argument does not escape

這次str也逃逸到堆上面了,在堆上面進行分配,因為入參是interface,變數str的地址被以實參的方式傳入fmt.Println被裝箱到一個interface{}

裝箱的形參變數要在堆上分配,但是還需要儲存一個棧上的地址,這和之前說的第一條不符,所以str也會分配到堆上

3.閉包產生逃逸

func Increase() func() int {
 n := 0
 return func() int {
  n++
  return n
 }
}

func main() {
 in := Increase()
 fmt.Println(in()) // 1
}

E:GoStudysrcHighBaseEscape>go build -gcflags "-m -m -l" ./pointer.go
# command-line-arguments
.pointer.go:27:2: Increase capturing by ref: n (addr=false assign=true width=8)
.pointer.go:28:9: func literal escapes to heap:
.pointer.go:28:9:   flow: ~r0 = &{storage for func literal}:
.pointer.go:28:9:     from func literal (spill) at .pointer.go:28:9
.pointer.go:28:9:     from return func literal (return) at .pointer.go:28:2
.pointer.go:27:2: n escapes to heap:
.pointer.go:27:2:   flow: {storage for func literal} = &n:
.pointer.go:27:2:     from n (captured by a closure) at .pointer.go:29:3
.pointer.go:27:2:     from n (reference) at .pointer.go:29:3
.pointer.go:27:2: moved to heap: n
.pointer.go:28:9: func literal escapes to heap
.pointer.go:36:16: in() escapes to heap:
.pointer.go:36:16:   flow: {storage for ... argument} = &{storage for in()}:
.pointer.go:36:16:     from in() (spill) at .pointer.go:36:16
.pointer.go:36:16:     from ... argument (slice-literal-element) at .pointer.go:36:13
.pointer.go:36:16:   flow: {heap} = {storage for ... argument}:
.pointer.go:36:16:     from ... argument (spill) at .pointer.go:36:13
.pointer.go:36:16:     from fmt.Println(... argument...) (call parameter) at .pointer.go:36:13
.pointer.go:36:13: ... argument does not escape
.pointer.go:36:16: in() escapes to heap

因為函數是指標型別,所以匿名函數當做返回值產生逃逸,匿名函數使用外部變數n,這個n會一直存在知道in被銷燬

4. 變數大小不確定及棧空間不足引發逃逸

import (
    "math/rand"
)

func LessThan8192()  {
    nums := make([]int, 100) // = 64KB
    for i := 0; i < len(nums); i++ {
        nums[i] = rand.Int()
    }
}


func MoreThan8192(){
    nums := make([]int, 1000000) // = 64KB
    for i := 0; i < len(nums); i++ {
        nums[i] = rand.Int()
    }
}


func NonConstant() {
    number := 10
    s := make([]int, number)
    for i := 0; i < len(s); i++ {
        s[i] = i
    }
}

func main() {
    NonConstant()
    MoreThan8192()
    LessThan8192()
}

# command-line-arguments
.pointer.go:43:14: make([]int, 100) does not escape
.pointer.go:51:14: make([]int, 1000000) escapes to heap:
.pointer.go:51:14:   flow: {heap} = &{storage for make([]int, 1000000)}:
.pointer.go:51:14:     from make([]int, 1000000) (too large for stack) at .pointer.go:51:14
.pointer.go:51:14: make([]int, 1000000) escapes to heap
.pointer.go:60:11: make([]int, number) escapes to heap:
.pointer.go:60:11:   flow: {heap} = &{storage for make([]int, number)}:
.pointer.go:60:11:     from make([]int, number) (non-constant size) at .pointer.go:60:11
.pointer.go:60:11: make([]int, number) escapes to heap

棧空間足夠不會發生逃逸,但是變數過大,已經超過棧空間,會逃逸到堆上

5.總結

  • 逃逸分析在編譯階段確定哪些變數可以分配在棧中,哪些變數分配在堆上
  • 逃逸分析減輕了GC壓力,提高程式的執行速度
  • 棧上記憶體使用完畢不需要GC處理,堆上記憶體使用完畢會交給GC處理
  • 函數傳參時對於需要修改原物件值,或佔用記憶體比較大的結構體,選擇傳指標。對於唯讀的佔用記憶體較小的結構體,直接傳值能夠獲得更好的效能
  • 根據程式碼具體分析,儘量減少逃逸程式碼,減輕GC壓力,提高效能

到此這篇關於淺談Golang記憶體逃逸 的文章就介紹到這了,更多相關Golang記憶體逃逸 內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com