一篇介紹堆和棧的好文章

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ā)生逃逸。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Lua 5.1 參考手冊(cè) by Roberto Ierusalimschy, Luiz Henrique de F...
    蘇黎九歌閱讀 14,256評(píng)論 0 38
  • 一、預(yù)備知識(shí)—程序的內(nèi)存分配 一個(gè)由c/C++編譯的程序占用的內(nèi)存分為以下幾個(gè)部分1、棧區(qū)(stack)— 由編譯...
    瓊胖子閱讀 634評(píng)論 0 0
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒(méi)有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對(duì)...
    cosWriter閱讀 11,666評(píng)論 1 32
  • 一、溫故而知新 1. 內(nèi)存不夠怎么辦 內(nèi)存簡(jiǎn)單分配策略的問(wèn)題地址空間不隔離內(nèi)存使用效率低程序運(yùn)行的地址不確定 關(guān)于...
    SeanCST閱讀 8,133評(píng)論 0 27
  • 前言本系列文章總共包括 4 篇,主要幫助大家理解 Go 語(yǔ)言中一些語(yǔ)言機(jī)制和其背后的設(shè)計(jì)原則,包括指針、棧、堆、逃...
    沒(méi)我找不到電子書(shū)閱讀 461評(píng)論 0 0

友情鏈接更多精彩內(nèi)容