【C++學習筆記】啥叫堆?啥叫棧?

好好學習專用部落格發表於2017-04-03
通常——尤其是本帖——所說的“堆/棧”,其實是“堆/棧式記憶體分配”、“作業系統的堆/棧實現”、“作業系統實現的堆/棧管理器”、“語言的堆/棧實現及其管理器”等等等等一大筐概念的大雜燴。關於這種大雜燴的討論過於高深莫測,非俺這等低水平人士所能置喙。

所以,低水平回覆嘛,第一步得容俺先zhuangbility一下,把這一堆大雜燴“正交分解”,然後一個一個慢慢說清楚。


首先,啥叫“堆式記憶體分配”和“棧式記憶體分配”?各有什麼優缺點?

堆式記憶體分配是記憶體管理的一種方式。這個管理方法對記憶體的申請、釋放沒有特殊要求,只要你記得申請了就要釋放就好。
這種管理方法通用性好(甚至可以模擬棧^_^),但實現複雜,而且有一堆一堆的諸如記憶體碎片問題、記憶體池演算法適用性和效率等等問題需要處理。

棧式記憶體分配則嚴格限制了記憶體的申請和分配的次序。它要求先申請的記憶體不能在後申請的記憶體歸還之前歸還。這就導致它的用途非常單一,侷限於特定的場景(函式呼叫)和某些特定演算法。但也正因為它的單一,使得它的管理演算法簡單,速度快。加上加速、簡化函式/過程呼叫設計的需要,幾乎所有現代CPU都內建了push/pop之類的棧操作指令,大大加速和簡化了函式呼叫/返回功能程式碼。





顯然,堆式管理的記憶體就是堆,被棧式管理的記憶體自然就是棧。

那麼,堆和棧都在哪裡?

無處不在。

不僅無處不在,它們還彼此巢狀、改頭換面、難解難分。



比如,一個程式誕生了,作業系統要做些什麼?

1、使用自己的堆管理器,為它申請一塊空間當作程式控制塊
2、在程式控制塊裡,為它配備一個堆管理器,幫它管理它的3G(或更大)記憶體空間
3、在程式堆裡,替它申請2M位元組(可由ulimit改變大小)空間當作執行棧;然後把這個空間一端的地址放到CPU的棧基址暫存器裡,這樣就可以利用CPU的push/pop系列指令自動替他管理這個棧(相當於硬體實現的棧空間管理器)

——大多情況下,人們所提到的堆和棧,其實就是這個作業系統為程式設定的堆/棧。

然後,這個程式建立了一個執行緒,又發生了什麼?

1、讓它仍然使用程式的堆管理器
2、為它另外申請xM位元組作為棧空間
3、在系統空間(這點視作業系統而定)為它申請一個執行緒控制塊,把程式堆管理器的地址記錄於其中,把棧地址記錄於其中

前面有高人質問:沒有棧,你怎麼實現執行緒?
嗯,低水平回答:很明顯,沒有堆,執行緒才會無法建立;有棧沒棧關執行緒實現鳥事。
至於棧嘛……它是為了方便函式/過程呼叫的。

那麼,沒有棧,函式/過程呼叫操作是否就沒法進行了?

答案是:
1、慢一點點而已。注意堆是可以模擬棧的,只要把原來的push+call和pop+ret操作對替換為new/malloc+call和delete/free+ret對即可。
2、拋棄了棧的過程呼叫,反而可能得到騰飛的機會。
比如近年火熱的函數語言程式設計,實際就是想方設法使得函式呼叫無副作用;這樣一來,每個函式都可以“丟”到另外一個執行緒裡去亂序執行——這是一個特別適合平行計算的設計。



迴歸正題,玩個BT的:
void fun(void)
{
    char buf[4*1024*1024];    //棧上申請4M記憶體
    tp=thread(thread_fun(buf));        //thread_fun裡面實現了一個堆管理器,用以管理buf的使用
    wait_thread(tp);
}

這個buf是堆,還是棧?在哪裡是堆,在哪裡是棧?



可見,特定時刻特定記憶體屬於堆還是棧,只和該記憶體當前的被管理方式相關;從一個堆上得到的記憶體完全可以在接下來當作棧來管理;從一個棧上得來的記憶體,同樣也沒人禁止你拿它當堆來提供給他人使用。

作為堆/棧的使用者,你不需要關心記憶體來自何處、由什麼方案管理——你只要知道如何申請、如何歸還就好了,最多再多知道點效能資料;作為堆/棧的實現者,如何得到記憶體並不會妨礙你的管理機制;你的使用者從你這裡取得記憶體做了什麼,你同樣無權干涉。



所以,想討論哪塊記憶體是從堆上得來還是棧上得來,首先必須明確這裡提到的堆/棧究竟是哪一級、由誰實現的。

比如,某個平臺的某種C編譯器上,malloc是CRT的堆記憶體管理器提供的介面,它要向作業系統的堆記憶體管理器申請記憶體,然後以堆的模式分配給使用者使用;函式區域性變數則是從程式棧裡分配的記憶體,這個記憶體又是從作業系統管理的堆裡面分配的。
而java虛擬機器中,虛擬機器自身用了宿主系統的堆和棧,但它上面跑的程式所用的堆和棧卻完全和宿主系統無關;某些語言甚至可能完全沒有堆/棧的概念——雖然最終它還是要使用作業系統的堆/棧。
而apache這類實現中,會先從CRT得到記憶體,然後把這些記憶體用它自己的堆管理演算法管理(可以搜尋下apache的記憶體池實現);那麼這時候利用apache的介面申請記憶體,用的就是apache的堆。
c++則另外實現了個new/delete介面的堆管理演算法(雖然其內部實現可能只是簡單的呼叫系統堆分配演算法);而STL則把這個堆管理演算法擴充了池管理功能——這個記憶體池曾經被我替換掉,因為我只需要用stl::hash_map管理我自己的海量同構簡單小物件,它的這個記憶體池過於複雜、緩慢了(當然,這個替換僅僅存在於使用stl::hash_map管理我的小物件時,其它時候用的還是stl的池或者c++的new/delete;當然,可怕的是,當時俺們組還有個牛人,相信微軟的VirtualAlloc分配的記憶體既不在堆上,也不在棧上,是速度最快的,所以大量使用了這個高階東東-.-)——有人能理清我這個系統的堆/棧結構嗎?



很顯然,不同的平臺上的不同編譯器下,堆/棧層次樹可能是非常不同的。

這些通通都不區分,卻熱衷於幫C委員會定義哪個東西來自於堆、哪個東西來自於棧——不好意思,這種高水平的討論,實在不是我等低水平人士所能理解。

相關文章