stack vs heap:栈区分配内存快还是堆区分配内存快 ?
作者 | 碼農的荒島求生
來源 | 碼農的荒島求生
有伙伴問到底是從棧上分配內存快還是從堆上分配內存快,這是個比較基礎的問題,今天就來聊一聊。
棧區的內存申請與釋放
毫無疑問,顯然從棧上分配內存更快,因為從棧上分配內存僅僅就是棧指針的移動而已,這是什么意思呢?什么叫做“棧指針的移動”?以x86平臺為例,在棧上分配內存是怎樣實現的呢?很簡單,就一行指令:
sub?$0x40,%rsp這行代碼就叫做“棧指針的移動”,其本質就是這張圖:
很簡單,寄存器esp中保存的是當前棧的棧頂地址,由于棧的增長方向是從高地址到低地址,因此增大棧時需要將棧指針向下移動,即sub指令的作用,這條指令將棧頂指針向下移動了64字節(0x40),因此可以說在棧上分配了64字節。
可以看到,在棧上分配內存其實非常非常簡單,簡單到就只有一條機器指令。
而棧區的內存釋放也非常簡單,也是只需要一條機器指令:
leaveleave指令的作用是將棧基址賦值給esp,這樣棧指針指向上一個棧幀的棧頂,然后pop出ebp,這樣ebp就指向上一個棧幀的棧底:
看到了吧,執行完leave指令后ebp以及esp就指向上一個棧幀了,這就相當于棧幀的彈出,pop,這樣stack 1占用的內存就無效了,沒有任何用處了,顯然這就是我們常說的內存回收,因此簡單的一條leave指令就可以回收掉棧區中的內存。
接下來我們看到堆區的內存申請與釋放。
堆區的內存申請與釋放
與棧區分配內存相對的是堆內存分配,堆區分配內存有多復雜呢?
在堆區上申請與釋放內存是一個相對復雜的過程,因為堆本身是需要程序員(內存分配器實現者)自己管理的,而棧是編譯器來維護的,堆區的維護同樣涉及內存的分配與釋放,但這里的內存分配與釋放顯然不會像棧區那樣簡單,一句話,這里是按需進行內存的分配與釋放,本質在于堆區中每一塊被分配出去的內存其生命周期都不一樣,這是由程序員決定的,我傾向于把內存動態分配釋放想象成去停車場找停車位。
這顯然會讓問題復雜起來,我們必須小心的維護哪些內存是已經分配出去的以及哪些是空閑的、該怎樣找到一塊空閑的內存、該怎樣回收程序員不需要的內存塊、同時還不能有嚴重的內存碎片問題,棧區分配釋放內存都無需關心這些問題,于此同時當堆區內存空間不足時還需要擴大堆區等等,這些都使得在堆區申請內存要比在棧區分配內存復雜的多。
說了這么多,那么在堆區上申請內存要比在棧上申請內存慢多少呢?
接下來我們寫段代碼實驗一下。
代碼
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???%rbp400f86:???????48?89?e5????????????????mov????%rsp,%rbp400f89:???????5d??????????????????????pop????%rbp400f8a:???????c3??????????????????????retqtest_on_heapv:400f8b:???????55??????????????????????push???%rbp400f8c:???????48?89?e5????????????????mov????%rsp,%rbp400f8f:???????5d??????????????????????pop????%rbp400f90:???????c3??????????????????????retq啊哈,編譯器實在是太聰明了,它顯然注意到這兩個函數中的代碼實際上啥也沒干,即使我們還專門為變量a賦值為了10,但后續我們根本就沒有用到變量a,因此編譯器給我們生成了一個空函數,上面這些機器指令實際上對應一個空函數。
小風哥反復在這里添加代碼都沒有騙過編譯器,我試圖加大變量a賦值的復雜度,編譯器依然很聰明的生成了一個空函數,反正我是沒有試出來,可見現代編譯器是足夠智能的,生成的機器指令效率很高,關于該怎樣寫出一個更好的benchmark,從而讓我們可以看到在開啟編譯優化的情況下這兩種內存分配方式的對比,歡迎任何對此有心得或者對編譯優化有心得的同學留言。
最后讓我們來看看這兩種內存分配方式的定位。
棧內存與堆內存的差異
首先我們必須意識到,棧是一種先進后出的結構,棧區會隨著函數調用層級的增加而增大,而隨著函數調用完成而減少,因此棧是無需任何“管理”的;與此同時由于棧的這種性質,在棧上申請的內存其生命周期是和函數綁定在一起,當函數調用完成后其占用的棧幀內存將無效,且棧的大小是有限的,你不能在棧上申請過多內存,就像這樣一段C代碼:
void?test()?{int?b[10000000];b[1000000]?=?10; }這段代碼運行起來后會core掉,原因就在于棧區大小是非常有限的,在棧上分配一大塊數據會讓棧撐爆掉,這就是所謂的Stack Overflow:
額。。。不好意思,圖放錯了,應該是這個Stack Overflow:
不好意思,又放錯了,總之你懂得。
而堆則不同,在堆上分配的內存其生命周期是受程序員控制的,程序員決定什么時候申請內存,什么時候釋放內存,因此堆是必須被管理起來的,堆區是一片很廣闊的區域,堆區空間不足時會向操作系統請求擴大堆區從而獲得更多地址空間。
當然,堆區在給程序員更大靈活性的同時需要程序員確保內存在不被使用時釋放掉,否則會內存泄漏,在棧上申請內存則不存這個問題。
總結
棧區是自動管理的,堆區是手動管理的,顯然在棧區上分配內存要比在堆區上更快,當在棧區上申請的內存使用場景有限,程序員申請內存時還要更多的依靠堆區,但是在棧區申請的內存滿足要求的情況我個人更傾向于使用棧區內存。
希望這篇文章對大家理解堆區棧區有所幫助。
往期推薦
Redis 緩存擊穿(失效)、緩存穿透、緩存雪崩怎么解決?
如果被問到分布式鎖,應該怎樣回答?
性能突出的 Redis 是咋使用 epoll 的?
Java 底層知識:什么是?“橋接方法”??
點分享
點收藏
點點贊
點在看
總結
以上是生活随笔為你收集整理的stack vs heap:栈区分配内存快还是堆区分配内存快 ?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 移动云亮相 2021 IDC 年度盛典
- 下一篇: 源码级别的广播与监听实现