stack vs heap:棧區分配內存快還是堆區分配內存快 ?
大家好,我是小風哥。
後臺有讀者問到底是從棧上分配內存快還是從堆上分配內存快,這是個比較基礎的問題,今天就來聊一聊。
棧區的內存申請與釋放
毫無疑問,顯然從棧上分配內存更快,因爲從棧上分配內存僅僅就是棧指針的移動而已,這是什麼意思呢?什麼叫做 “棧指針的移動”?以 x86 平臺爲例,在棧上分配內存是怎樣實現的呢?很簡單,就一行指令:
sub $0x40,%rsp
這行代碼就叫做 “棧指針的移動”,其本質就是這張圖:
很簡單,寄存器 esp 中保存的是當前棧的棧頂地址,由於棧的增長方向是從高地址到低地址,因此增大棧時需要將棧指針向下移動,即 sub 指令的作用,這條指令將棧頂指針向下移動了 64 字節 (0x40),因此可以說在棧上分配了 64 字節。
可以看到,在棧上分配內存其實非常非常簡單,簡單到就只有一條機器指令。
而棧區的內存釋放也非常簡單,也是隻需要一條機器指令:
leave
leave 指令的作用是將棧基址賦值給 esp,這樣棧指針指向上一個棧幀的棧頂,然後 pop 出 ebp,這樣 ebp 就指向上一個棧幀的棧底:
看到了吧,執行完 leave 指令後 ebp 以及 esp 就指向上一個棧幀了,這就相當於棧幀的彈出,pop,這樣 stack 1 佔用的內存就無效了,沒有任何用處了,顯然這就是我們常說的內存回收,因此簡單的一條 leave 指令就可以回收掉棧區中的內存。
關於棧、棧幀與棧區,更詳細的講解可以參考我寫的這篇《函數運行時在內存中是什麼樣子?》。
接下來我們看到堆區的內存申請與釋放。
堆區的內存申請與釋放
與棧區分配內存相對的是堆內存分配,堆區分配內存有多複雜呢?複雜到我用了兩篇文章來講解堆內存分配的實現原理《自己動手實現 malloc 內存分配器》《申請內存時底層發生了什麼?》。
在堆區上申請與釋放內存是一個相對複雜的過程,因爲堆本身是需要程序員 (內存分配器實現者) 自己管理的,而棧是編譯器來維護的,堆區的維護同樣涉及內存的分配與釋放,但這裏的內存分配與釋放顯然不會像棧區那樣簡單,一句話,這裏是按需進行內存的分配與釋放,本質在於堆區中每一塊被分配出去的內存其生命週期都不一樣,這是由程序員決定的,我傾向於把內存動態分配釋放想象成去停車場找停車位。
這顯然會讓問題複雜起來,我們必須小心的維護哪些內存是已經分配出去的以及哪些是空閒的、該怎樣找到一塊空閒的內存、該怎樣回收程序員不需要的內存塊、同時還不能有嚴重的內存碎片問題,棧區分配釋放內存都無需關心這些問題,於此同時當堆區內存空間不足時還需要擴大堆區等等,這些都使得在堆區申請內存要比在棧區分配內存複雜的多,具體可以參考我寫的這兩篇《自己動手實現 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)<<endl;
begin = GetTimeStampInUs();
for (int i = 0; i < 100000000; ++i) {
test_on_heap();
}
cout<<"test on heap "<<((GetTimeStampInUs() - begin) / 1000000.0)<<endl;
}
這段代碼非常簡單,這裏有兩個函數:
-
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 Overflow:
不好意思,又放錯了,總之你懂得。
而堆則不同,在堆上分配的內存其生命週期是受程序員控制的,程序員決定什麼時候申請內存,什麼時候釋放內存,因此堆是必須被管理起來的,堆區是一片很廣闊的區域,堆區空間不足時會向操作系統請求擴大堆區從而獲得更多地址空間。
當然,堆區在給程序員更大靈活性的同時需要程序員確保內存在不被使用時釋放掉,否則會內存泄漏,在棧上申請內存則不存這個問題。
總結
棧區是自動管理的,堆區是手動管理的,顯然在棧區上分配內存要比在堆區上更快,當在棧區上申請的內存使用場景有限,程序員申請內存時還要更多的依靠堆區,但是在棧區申請的內存滿足要求的情況我個人更傾向於使用棧區內存。
希望這篇文章對大家理解堆區棧區有所幫助。
碼農的荒島求生 底層的任何疑惑都能在這找到答案
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/6I5TTh7zJ4NAn8YALUEtuw