您當前的位置:首頁 > 體育

stack vs heap:棧區分配記憶體快還是堆區分配記憶體快 ?

作者:由 碼農的荒島求生 發表于 體育時間:2022-03-17

大家好,我是小風哥。

後臺有讀者問到底是從棧上分配記憶體快還是從堆上分配記憶體快,這是個比較基礎的問題,今天就來聊一聊。

棧區的記憶體申請與釋放

毫無疑問,顯然從棧上分配記憶體更快,因為從棧上分配記憶體僅僅就是棧指標的移動而已,這是什麼意思呢?什麼叫做“棧指標的移動”?以x86平臺為例,在棧上分配記憶體是怎樣實現的呢?很簡單,就一行指令:

sub$0x40,%rsp

這行程式碼就叫做“棧指標的移動”,其本質就是這張圖:

stack vs heap:棧區分配記憶體快還是堆區分配記憶體快 ?

很簡單,暫存器esp中儲存的是當前棧的棧頂地址,由於棧的增長方向是從高地址到低地址,因此增大棧時需要將棧指標向下移動,即sub指令的作用,這條指令將棧頂指標向下移動了64位元組(0x40),因此可以說在棧上分配了64位元組。

可以看到,在棧上分配記憶體其實非常非常簡單,

簡單到就只有一條機器指令

而棧區的記憶體釋放也非常簡單,也是隻需要一條機器指令:

leave

leave指令的作用是將棧基址賦值給esp,這樣棧指標指向上一個棧幀的棧頂,然後pop出ebp,這樣ebp就指向上一個棧幀的棧底:

stack vs heap:棧區分配記憶體快還是堆區分配記憶體快 ?

看到了吧,執行完leave指令後ebp以及esp就指向上一個棧幀了,這就相當於棧幀的彈出,pop,這樣stack 1佔用的記憶體就無效了,沒有任何用處了,顯然這就是我們常說的記憶體回收,因此

簡單的一條leave指令就可以回收掉棧區中的記憶體

stack vs heap:棧區分配記憶體快還是堆區分配記憶體快 ?

關於棧、棧幀與棧區,更詳細的講解可以參考我寫的這篇《函式執行時在記憶體中是什麼樣子?》。

接下來我們看到堆區的記憶體申請與釋放。

堆區的記憶體申請與釋放

與棧區分配記憶體相對的是堆記憶體分配,堆區分配記憶體有多複雜呢?複雜到我用了兩篇文章來講解堆記憶體分配的實現原理《自己動手實現malloc記憶體分配器》《申請記憶體時底層發生了什麼?》。

在堆區上申請與釋放記憶體是一個相對複雜的過程,因為堆本身是需要程式設計師(記憶體分配器實現者)自己管理的,而棧是編譯器來維護的,堆區的維護同樣涉及記憶體的分配與釋放,但這裡的記憶體分配與釋放顯然不會像棧區那樣簡單,一句話,這裡是

按需進行記憶體的分配與釋放

本質在於堆區中每一塊被分配出去的記憶體其生命週期都不一樣

,這是由程式設計師決定的,我傾向於把記憶體動態分配釋放想象成去停車場找停車位。

stack vs heap:棧區分配記憶體快還是堆區分配記憶體快 ?

這顯然會讓問題複雜起來,我們必須小心的維護哪些記憶體是已經分配出去的以及哪些是空閒的、該怎樣找到一塊空閒的記憶體、該怎樣回收程式設計師不需要的記憶體塊、同時還不能有嚴重的記憶體碎片問題,棧區分配釋放記憶體都無需關心這些問題,於此同時當堆區記憶體空間不足時還需要擴大堆區等等,這些都使得在堆區申請記憶體要比在棧區分配記憶體複雜的多,具體可以參考我寫的這兩篇《自己動手實現malloc記憶體分配器》《申請記憶體時底層發生了什麼》。

說了這麼多,那麼在堆區上申請記憶體要比在棧上申請記憶體慢多少呢?

接下來我們寫段程式碼實驗一下。

show me the code

void test_on_stack() {

int a = 10;

}

void test_on_heap() {

int* a = (int*)malloc(sizeof(int));

*a = 10;

free(a);

}

void test() {

auto begin = GetTimeStampInUs();

for (int i = 0; i < 100000000; ++i) {

test_on_stack();

}

cout<<“test on stack ”<<((GetTimeStampInUs() - begin) / 1000000。0)<

begin = GetTimeStampInUs();

for (int i = 0; i < 100000000; ++i) {

test_on_heap();

}

cout<<“test on heap ”<<((GetTimeStampInUs() - begin) / 1000000。0)<

}

這段程式碼非常簡單,這裡有兩個函式:

test_on_stack函式中定義一個區域性變數,這就是從棧上申請一個整數大小的記憶體空間

test_on_heap函式從堆上申請一個整數大小的記憶體空間

然後我們在測試函式中分別呼叫這兩個函式,每一個呼叫1億次,記錄下需要執行的時間,得到的測試結果為:

test on stack 0。191008

test on heap 20。0215

可以看到,在棧上總耗時只有大概0。2s,而在堆上分配的耗時為20s,相差百倍。

值得注意的是,這裡在編譯程式時沒有開啟編譯最佳化,開啟編譯最佳化後的耗時是這樣的:

test on stack 0。033521

test on heap 0。039294

可以看到,相差無幾,可這是為什麼呢?顯然從常理推斷在棧上分配要更快一些,問題會出在哪裡呢?

既然我們開啟了編譯最佳化,那是不是最佳化後的程式碼執行的更快了呢,我們來看下編譯最佳化後生成的指令都有啥:

test_on_stackv:

400f85: 55 push %rbp

400f86: 48 89 e5 mov %rsp,%rbp

400f89: 5d pop %rbp

400f8a: c3 retq

test_on_heapv:

400f8b: 55 push %rbp

400f8c: 48 89 e5 mov %rsp,%rbp

400f8f: 5d pop %rbp

400f90: c3 retq

啊哈,編譯器實在是太聰明瞭,它顯然注意到這兩個函式中的程式碼實際上啥也沒幹,即使我們還專門為變數a賦值為了10,但後續我們根本就沒有用到變數a,因此編譯器給我們生成了一個空函式,上面這些機器指令實際上對應一個空函式。

小風哥反覆在這裡新增程式碼都沒有騙過編譯器,我試圖加大變數a賦值的複雜度,編譯器依然很聰明的生成了一個空函式,反正我是沒有試出來,

可見現代編譯器是足夠智慧的

,生成的機器指令效率很高,關於該怎樣寫出一個更好的benchmark,從而讓我們可以看到在開啟編譯最佳化的情況下這兩種記憶體分配方式的對比,歡迎任何對此有心得或者對編譯最佳化有心得的同學留言。

最後讓我們來看看這兩種記憶體分配方式的定位。

棧記憶體與堆記憶體的差異

首先我們必須意識到,棧是一種先進後出的結構,棧區會隨著函式呼叫層級的增加而增大,而隨著函式呼叫完成而減少,因此棧是無需任何“管理”的;與此同時由於棧的這種性質,在棧上申請的記憶體其生命週期是和函式繫結在一起,當函式呼叫完成後其佔用的棧幀記憶體將無效,且棧的大小是有限的,你不能在棧上申請過多記憶體,就像這樣一段C程式碼:

void test() {

int b[10000000];

b[1000000] = 10;

}

這段程式碼執行起來後會core掉,原因就在於棧區大小是非常有限的,在棧上分配一大塊資料會讓棧撐爆掉,這就是所謂的Stack Overflow:

stack vs heap:棧區分配記憶體快還是堆區分配記憶體快 ?

額。。。不好意思,圖放錯了,應該是這個Stack Overflow:

stack vs heap:棧區分配記憶體快還是堆區分配記憶體快 ?

不好意思,又放錯了,總之你懂得。

而堆則不同,在堆上分配的記憶體其生命週期是受程式設計師控制的,程式設計師決定什麼時候申請記憶體,什麼時候釋放記憶體,因此堆是必須被管理起來的,堆區是一片很廣闊的區域,堆區空間不足時會向作業系統請求擴大堆區從而獲得更多地址空間。

當然,堆區在給程式設計師更大靈活性的同時需要程式設計師確保記憶體在不被使用時釋放掉,否則會記憶體洩漏,在棧上申請記憶體則不存這個問題。

總結

棧區是自動管理的,堆區是手動管理的,顯然在棧區上分配記憶體要比在堆區上更快,當在棧區上申請的記憶體使用場景有限,程式設計師申請記憶體時還要更多的依靠堆區,但是在棧區申請的記憶體滿足要求的情況我個人更傾向於使用棧區記憶體。

希望這篇文章對大家理解堆區棧區有所幫助。