Go 中的分段棧和連續(xù)棧的區(qū)別?
# 分段棧
在 Go 1.3 版本之前 ,使用的棧結(jié)構(gòu)是分段棧,隨著goroutine 調(diào)用的函數(shù)層級(jí)的深入或者局部變量需要的越來(lái)越多時(shí),運(yùn)行時(shí)會(huì)調(diào)用 runtime.morestack 和 runtime.newstack創(chuàng)建一個(gè)新的??臻g,這些棧空間是不連續(xù)的,但是當(dāng)前 goroutine 的多個(gè)??臻g會(huì)以雙向鏈表的形式串聯(lián)起來(lái),運(yùn)行時(shí)會(huì)通過(guò)指針找到連續(xù)的棧片段。
分段棧雖然能夠按需為當(dāng)前 goroutine 分配內(nèi)存并且及時(shí)減少內(nèi)存的占用,但是它也存在一個(gè)比較大的問(wèn)題:
如果當(dāng)前 goroutine 的棧幾乎充滿(mǎn),那么任意的函數(shù)調(diào)用都會(huì)觸發(fā)棧的擴(kuò)容,當(dāng)函數(shù)返回后又會(huì)觸發(fā)棧的收縮,如果在一個(gè)循環(huán)中調(diào)用函數(shù),棧的分配和釋放就會(huì)造成巨大的額外開(kāi)銷(xiāo),這被稱(chēng)為熱分裂問(wèn)題(Hot split)。
為了解決這個(gè)問(wèn)題,Go 在 1.2 版本的時(shí)候不得不將棧的初始化內(nèi)存從 4KB 增大到了 8KB。后來(lái)把采用連續(xù)棧結(jié)構(gòu)后,又把初始棧大小減小到了 2KB。
# 連續(xù)棧
連續(xù)??梢越鉀Q分段棧中存在的兩個(gè)問(wèn)題,其核心原理就是每當(dāng)程序的棧空間不足時(shí),初始化一片比舊棧大兩倍的新棧并將原棧中的所有值都遷移到新的棧中,新的局部變量或者函數(shù)調(diào)用就有了充足的內(nèi)存空間。使用連續(xù)棧機(jī)制時(shí),??臻g不足導(dǎo)致的擴(kuò)容會(huì)經(jīng)歷以下幾個(gè)步驟:
- 調(diào)用用 runtime.newstack 在內(nèi)存空間中分配更大的棧內(nèi)存空間;
- 使用 runtime.copystack 將舊棧中的所有內(nèi)容復(fù)制到新的棧中;
- 將指向舊棧對(duì)應(yīng)變量的指針重新指向新棧;
- 調(diào)用 runtime.stackfree銷(xiāo)毀并回收舊棧的內(nèi)存空間;
copystack會(huì)把舊棧里的所有內(nèi)容拷貝到新棧里然后調(diào)整所有指向舊棧的變量的指針指向到新棧, 我們可以用下面這個(gè)程序驗(yàn)證下,棧擴(kuò)容后同一個(gè)變量的內(nèi)存地址會(huì)發(fā)生變化。
- 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`)
- }
程序的輸出可以看到在棧擴(kuò)容前后,變量x的內(nèi)存地址的變化:
- 0xc000030738
- ...
- ...
- 0xc000081f38
是不是很簡(jiǎn)單呢?
本文轉(zhuǎn)載自微信公眾號(hào)「Go編程時(shí)光」,可以通過(guò)以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系Go編程時(shí)光公眾號(hào)。