http://c.biancheng.net/view/22.html
什么是棧
棧(Stack)是一種擁有特殊規(guī)則的線性表數(shù)據(jù)結(jié)構(gòu)。
1) 概念
棧只允許往線性表的一端放入數(shù)據(jù),之后在這一端取出數(shù)據(jù),按照后進(jìn)先出(LIFO,Last InFirst Out)的順序,如下圖所示。
圖:棧的操作及擴(kuò)展
往棧中放入元素的過(guò)程叫做入棧。入棧會(huì)增加棧的元素?cái)?shù)量,最后放入的元素總是位于棧的頂部,最先放入的元素總是位于棧的底部。
從棧中取出元素時(shí),只能從棧頂部取出。取出元素后,棧的數(shù)量會(huì)變少。最先放入的元素總是最后被取出,最后放入的元素總是最先被取出。不允許從棧底獲取數(shù)據(jù),也不允許對(duì)棧成員(除棧頂外的成員)進(jìn)行任何查看和修改操作。
棧的原理類似于將書(shū)籍一本一本地堆起來(lái)。書(shū)按順序一本一本從頂部放入,要取書(shū)時(shí)只能從頂部一本一本取出。
2) 變量和棧有什么關(guān)系
??捎糜趦?nèi)存分配,棧的分配和回收速度非常快。下面代碼展示棧在內(nèi)存分配上的作用,代碼如下:
func calc(a, b int) int {
? ? var c int
? ? c = a * b
? ? var x int
? ? x = c * 10
? ? return x
}
代碼說(shuō)明如下:
第 1 行,傳入 a、b 兩個(gè)整型參數(shù)。
第 2 行,聲明 c 整型變量,運(yùn)行時(shí),c 會(huì)分配一段內(nèi)存用以存儲(chǔ) c 的數(shù)值。
第 3 行,將 a 和 b 相乘后賦予 c。
第 5 行,聲明 x 整型變量,x 也會(huì)被分配一段內(nèi)存。
第 6 行,讓 c 乘以 10 后存儲(chǔ)到 x 變量中。
第 8 行,返回 x 的值。
上面的代碼在沒(méi)有任何優(yōu)化情況下,會(huì)進(jìn)行 c 和 x 變量的分配過(guò)程。Go 語(yǔ)言默認(rèn)情況下會(huì)將 c 和 x 分配在棧上,這兩個(gè)變量在 calc() 函數(shù)退出時(shí)就不再使用,函數(shù)結(jié)束時(shí),保存 c 和 x 的棧內(nèi)存再出棧釋放內(nèi)存,整個(gè)分配內(nèi)存的過(guò)程通過(guò)棧的分配和回收都會(huì)非常迅速。
什么是堆
堆在內(nèi)存分配中類似于往一個(gè)房間里擺放各種家具,家具的尺寸有大有小。分配內(nèi)存時(shí),需要找一塊足夠裝下家具的空間再擺放家具。經(jīng)過(guò)反復(fù)擺放和騰空家具后,房間里的空間會(huì)變得亂七八糟,此時(shí)再往空間里擺放家具會(huì)存在雖然有足夠的空間,但各空間分布在不同的區(qū)域,無(wú)法有一段連續(xù)的空間來(lái)擺放家具的問(wèn)題。此時(shí),內(nèi)存分配器就需要對(duì)這些空間進(jìn)行調(diào)整優(yōu)化,如下圖所示。
圖:堆的分配及空間
堆分配內(nèi)存和棧分配內(nèi)存相比,堆適合不可預(yù)知大小的內(nèi)存分配。但是為此付出的代價(jià)是分配速度較慢,而且會(huì)形成內(nèi)存碎片。
變量逃逸(Escape Analysis)——自動(dòng)決定變量分配方式,提高運(yùn)行效率
堆和棧各有優(yōu)缺點(diǎn),該怎么在編程中處理這個(gè)問(wèn)題呢?在 C/C++ 語(yǔ)言中,需要開(kāi)發(fā)者自己學(xué)習(xí)如何進(jìn)行內(nèi)存分配,選用怎樣的內(nèi)存分配方式來(lái)適應(yīng)不同的算法需求。比如,函數(shù)局部變量盡量使用棧;全局變量、結(jié)構(gòu)體成員使用堆分配等。程序員不得不花費(fèi)很多年的時(shí)間在不同的項(xiàng)目中學(xué)習(xí)、記憶這些概念并加以實(shí)踐和使用。
Go 語(yǔ)言將這個(gè)過(guò)程整合到編譯器中,命名為“變量逃逸分析”。這個(gè)技術(shù)由編譯器分析代碼的特征和代碼生命期,決定應(yīng)該如何堆還是棧進(jìn)行內(nèi)存分配,即使程序員使用 Go 語(yǔ)言完成了整個(gè)工程后也不會(huì)感受到這個(gè)過(guò)程。
1) 逃逸分析
使用下面的代碼來(lái)展現(xiàn) Go 語(yǔ)言如何通過(guò)命令行分析變量逃逸,代碼如下:
package main
import "fmt"
// 本函數(shù)測(cè)試入口參數(shù)和返回值情況
func dummy(b int) int {
? ? // 聲明一個(gè)c賦值進(jìn)入?yún)?shù)并返回
? ? var c int
? ? c = b
? ? return c
}
// 空函數(shù), 什么也不做
func void() {
}
func main() {
? ? // 聲明a變量并打印
? ? var a int
? ? // 調(diào)用void()函數(shù)
? ? void()
? ? // 打印a變量的值和dummy()函數(shù)返回
? ? fmt.Println(a, dummy(0))
}
代碼說(shuō)明如下:
第 6 行,dummy() 函數(shù)擁有一個(gè)參數(shù),返回一個(gè)整型值,測(cè)試函數(shù)參數(shù)和返回值分析情況。
第 9 行,聲明 c 變量,這里演示函數(shù)臨時(shí)變量通過(guò)函數(shù)返回值返回后的情況。
第 16 行,這是一個(gè)空函數(shù),測(cè)試沒(méi)有任何參數(shù)函數(shù)的分析情況。
第 23 行,在 main() 中聲明 a 變量,測(cè)試 main() 中變量的分析情況。
第 26 行,調(diào)用 void() 函數(shù),沒(méi)有返回值,測(cè)試 void() 調(diào)用后的分析情況。
第 29 行,打印 a 和 dummy(0) 的返回值,測(cè)試函數(shù)返回值沒(méi)有變量接收時(shí)的分析情況。
接著使用如下命令行運(yùn)行上面的代碼:
$ go run -gcflags "-m -l" main.go
使用 go run 運(yùn)行程序時(shí),-gcflags 參數(shù)是編譯參數(shù)。其中 -m 表示進(jìn)行內(nèi)存分配分析,-l 表示避免程序內(nèi)聯(lián),也就是避免進(jìn)行程序優(yōu)化。
運(yùn)行結(jié)果如下:
# command-line-arguments
./main.go:29:13: a escapes to heap
./main.go:29:22: dummy(0) escapes to heap
./main.go:29:13: main ... argument does not escape
0 0
程序運(yùn)行結(jié)果分析如下:
輸出第 2 行告知“main 的第 29 行的變量 a 逃逸到堆”。
第 3 行告知“dummy(0)調(diào)用逃逸到堆”。由于 dummy() 函數(shù)會(huì)返回一個(gè)整型值,這個(gè)值被 fmt.Println 使用后還是會(huì)在其聲明后繼續(xù)在 main() 函數(shù)中存在。
第 4 行,這句提示是默認(rèn)的,可以忽略。
上面例子中變量 c 是整型,其值通過(guò) dummy() 的返回值“逃出”了 dummy() 函數(shù)。c 變量值被復(fù)制并作為 dummy() 函數(shù)返回值返回,即使 c 變量在 dummy() 函數(shù)中分配的內(nèi)存被釋放,也不會(huì)影響 main() 中使用 dummy() 返回的值。c 變量使用棧分配不會(huì)影響結(jié)果。
2) 取地址發(fā)生逃逸
下面的例子使用結(jié)構(gòu)體做數(shù)據(jù),了解在堆上分配的情況,代碼如下:
package main
import "fmt"
// 聲明空結(jié)構(gòu)體測(cè)試結(jié)構(gòu)體逃逸情況
type Data struct {
}
func dummy() *Data {
? ? // 實(shí)例化c為Data類型
? ? var c Data
? ? //返回函數(shù)局部變量地址
? ? return &c
}
func main() {
? ? fmt.Println(dummy())
}
代碼說(shuō)明如下:
第 6 行,聲明一個(gè)空的結(jié)構(gòu)體做結(jié)構(gòu)體逃逸分析。
第 9 行,將 dummy() 函數(shù)的返回值修改為 *Data 指針類型。
第 12 行,將 c 變量聲明為 Data 類型,此時(shí) c 的結(jié)構(gòu)體為值類型。
第 15 行,取函數(shù)局部變量 c 的地址并返回。Go 語(yǔ)言的特性允許這樣做。
第 20 行,打印 dummy() 函數(shù)的返回值。
執(zhí)行逃逸分析:
$ go run -gcflags "-m -l" main.go
# command-line-arguments
./main.go:15:9: &c escapes to heap
./main.go:12:6: moved to heap: c
./main.go:20:19: dummy() escapes to heap
./main.go:20:13: main ... argument does not escape
&{}
注意第 4 行出現(xiàn)了新的提示:將 c 移到堆中。這句話表示,Go 編譯器已經(jīng)確認(rèn)如果將 c 變量分配在棧上是無(wú)法保證程序最終結(jié)果的。如果堅(jiān)持這樣做,dummy() 的返回值將是 Data 結(jié)構(gòu)的一個(gè)不可預(yù)知的內(nèi)存地址。這種情況一般是 C/C++ 語(yǔ)言中容易犯錯(cuò)的地方:引用了一個(gè)函數(shù)局部變量的地址。
Go 語(yǔ)言最終選擇將 c 的 Data 結(jié)構(gòu)分配在堆上。然后由垃圾回收器去回收 c 的內(nèi)存。
3) 原則
在使用 Go 語(yǔ)言進(jìn)行編程時(shí),Go 語(yǔ)言的設(shè)計(jì)者不希望開(kāi)發(fā)者將精力放在內(nèi)存應(yīng)該分配在棧還是堆上的問(wèn)題。編譯器會(huì)自動(dòng)幫助開(kāi)發(fā)者完成這個(gè)糾結(jié)的選擇。但變量逃逸分析也是需要了解的一個(gè)編譯器技術(shù),這個(gè)技術(shù)不僅用于 Go 語(yǔ)言,在 Java 等語(yǔ)言的編譯器優(yōu)化上也使用了類似的技術(shù)。
編譯器覺(jué)得變量應(yīng)該分配在堆和棧上的原則是:
變量是否被取地址。
變量是否發(fā)生逃逸。