Go 中的分段棧和連續棧的區別?
# 分段棧
在 Go 1.3 版本之前 ,使用的棧結構是分段棧,隨著goroutine 調用的函數層級的深入或者局部變量需要的越來越多時,運行時會調用 runtime.morestack 和 runtime.newstack創建一個新的棧空間,這些棧空間是不連續的,但是當前 goroutine 的多個棧空間會以雙向鏈表的形式串聯起來,運行時會通過指針找到連續的棧片段。
分段棧雖然能夠按需為當前 goroutine 分配內存并且及時減少內存的占用,但是它也存在一個比較大的問題:
如果當前 goroutine 的棧幾乎充滿,那么任意的函數調用都會觸發棧的擴容,當函數返回后又會觸發棧的收縮,如果在一個循環中調用函數,棧的分配和釋放就會造成巨大的額外開銷,這被稱為熱分裂問題(Hot split)。
為了解決這個問題,Go 在 1.2 版本的時候不得不將棧的初始化內存從 4KB 增大到了 8KB。后來把采用連續棧結構后,又把初始棧大小減小到了 2KB。
# 連續棧
連續棧可以解決分段棧中存在的兩個問題,其核心原理就是每當程序的棧空間不足時,初始化一片比舊棧大兩倍的新棧并將原棧中的所有值都遷移到新的棧中,新的局部變量或者函數調用就有了充足的內存空間。使用連續棧機制時,棧空間不足導致的擴容會經歷以下幾個步驟:
- 調用用 runtime.newstack 在內存空間中分配更大的棧內存空間;
- 使用 runtime.copystack 將舊棧中的所有內容復制到新的棧中;
- 將指向舊棧對應變量的指針重新指向新棧;
- 調用 runtime.stackfree銷毀并回收舊棧的內存空間;
copystack會把舊棧里的所有內容拷貝到新棧里然后調整所有指向舊棧的變量的指針指向到新棧, 我們可以用下面這個程序驗證下,棧擴容后同一個變量的內存地址會發生變化。
- package main
- func main() {
- var x [10]int
- println(&x)
- a(x)
- println(&x)
- }
- //go:noinline
- func a(x [10]int) {
- println(`func a`)
- var y [100]int
- b(y)
- }
- //go:noinline
- func b(x [100]int) {
- println(`func b`)
- var y [1000]int
- c(y)
- }
- //go:noinline
- func c(x [1000]int) {
- println(`func c`)
- }
程序的輸出可以看到在棧擴容前后,變量x的內存地址的變化:
- 0xc000030738
- ...
- ...
- 0xc000081f38
是不是很簡單呢?
本文轉載自微信公眾號「Go編程時光」,可以通過以下二維碼關注。轉載本文請聯系Go編程時光公眾號。