內存篇之堆與棧的繞口令

    堆(heap)/(stackcall stack)是兩塊功能完全不同的系統內存區,堆內存是由malloc/free函數動態申請和回收,而棧則是編譯器與啓動代碼或線程創建代碼配合,約定用CPU某寄存器標識最新使用位置的一段內存(所以分系統棧與任務棧,見後文)。但不知是誰添亂,用堆棧這個詞代表的含義,導致中文裏堆棧混在一起,三者的關係就象繞口令,堆爲堆,棧是棧,堆棧是棧不是堆。爲避免混淆,後文一律不用堆棧,而用/表示。

    注:棧有時也指一種數據結構,特點是隻從一端(棧頂stack pointer)進行數據推入(push)和彈出(pop)操作。由於只允許單端操作,因而遵循後進先出(LIFO, Last In First Out)原則,(想想爲什麼),好比彈匣裏壓子彈,後壓進的子彈先發射。而形成對比的隊列結構則是從兩端分別讀寫數據,因而先進先出(FIFO),好比火車過隧道,車頭先進,也是車頭先出。

    本文所說的棧更確切的說法是棧段(stack segment),指採用這種結構工作的一段物理內存,用於存放局部變量函數返回地址等。棧段只需通過棧頂指針即可訪問,一些CPU有專用寄存器用於存放棧頂地址,另一些則定義某種規範,讓所有此CPU的編譯器都默認使用某通用寄存器(如ARMR13)存放sp指針。

    堆/棧的區分在相關bbs上似乎是一個永恆的話題,可見大量初學者對此混淆不清。這是因爲一來雖然堆/棧是軟件最基礎的概念之一,卻很少有書詳細論述,多數是隻言片語;再者很多人覺得堆/棧與具體編程無關,不需要深入研究。然而編程時即使寫出完全一樣的代碼,不同人的底氣也不一樣:有人有理有據所以胸有成竹,有人純粹瞎蒙寫完聽天由命。而深刻理解堆/棧,是跳出代碼搬運工層次的必需一步。看下面例子:

void alg_proc(char *infile, char *outfile)

{  

  char tmp_inbuf[3000000],tmp_outbuf[3000000];

  FILE *input=fopen(infile,”rb”);

  FILE *output=fopen(outfile,”wb”);

  fread(tmp_inbuf, 1, 3000000, input);

  algfun(tmp_inbuf, tmp_outbuf, BUF_LEN);

  fwrite(tmp_outbuf,1,3000000, output);

  ……

}

    乍一看沒什麼問題,初學者習慣用數組,不願碰指針,反正功能實現了,有容易的何必用難的J。但理解堆/棧後就會知道,這段程序在某些環境下可能無法運行。爲什麼?暫時賣個關子,先從幾方面對比理解堆/棧。

/棧初始化

    堆/棧都是在系統的啓動代碼裏預留並初始化,初始化階段結束後,系統就能爲用戶提供函數調用背後的進出棧支持以及動態堆內存管理支持。堆的初始化與後續管理方式相關,設定初始默認堆大小後,根據不同管理方式初始化鏈表/位圖/內存池甚至OS內核裏的相關結構和參數。棧是單端存取,初始化時只需設定棧頂地址(stack pointer,簡稱SP)和棧最大容量,具體說是由啓動代碼和內存佈局腳本文件配合,從系統整體內存中劃定一塊區域,並用一個CPU寄存器保存SP,可以是專門的硬件SP寄存器(如x86)或者標準規範指定某通用寄存器專門存儲SPARM),具體棧初始化過程後面專文論述。注意這裏指的是系統棧,任務棧是在線程創建時由用戶任意分配,不需要初始化。

分配與回收

    堆內存藉助malloc/free函數分配和回收,這裏的分配和回收,不象小孩子分糖果,分完就沒有了。堆內存只是被暫時使用,是對內存使用權的虛擬分配/回收,就象賓館把代表房間使用權的鑰匙分給客人,客人結帳時還要交還鑰匙。(站在管理者視角是分配/回收,用戶視角爲申請/釋放,實際一回事)。用戶通過malloc從堆內存區拿到某塊內存的鑰匙,這塊內存此後就被用戶獨佔,可在上面存儲數據,使用完則調用free,系統收回內存鑰匙。malloc/free在程序運行中動態調用,所以堆都是動態分配,沒有靜態堆。

    棧內存是編譯器自動分配和回收的內存區:棧區所保存的元素大小可統計,如局部變量/數組/函數參數等,所以編譯器就能提前把所有元素在棧中的位置安排好。相對堆在運行時才申請空間,棧是一種事先的離線的靜態分配(或者說安排更恰當)。做到這點還依賴於棧的單端存儲結構,不需要專門提供地址,按LIFO原則挪動相應SP指針就能實現內存的安排:壓棧是往SP指針所指內存寫數據,並遞增或遞減SP,出棧是把數據從SP所指內存讀到寄存器,並遞減或遞增SP(具體過程見本章節5)。

    堆的分配和使用都在運行階段發生,即先通過malloc返回一塊buffer的指針,然後用這塊buffer承載數據。棧在運行時直接使用,即通過sp指針讀寫棧內存。打個比方,堆分配是臨時買了一堆東西,然後在家裏到處找地方放;而棧是事先計劃好買多少東西,根據每件物品大小劃定好對應的空間,買好後不需要找,直接一件件放到事先規劃好的位置。事先規劃可以一個蘿蔔一個坑,但計劃又往往趕不上變化,所以堆/棧兩種方式都不可缺少。

空間大小

    爲防浪費,系統棧空間一般較小,因爲大容量棧固然能避免棧溢出,但程序觸及的最大棧深如果遠達不到系統棧底,剩下的部分就成了烏鴉喝不到的水。看開頭例子,函數中的局部數組char tmp_inbuf[3000000],意味着要在棧中存儲3000000bytes數據,這已超出多數系統棧的最大容量,因此是不實用的代碼。更進一步,涉及多線程編程時,線程棧的設置直接考驗程序員對棧的理解,要根據線程裏函數調用深度及局部變量使用情況估計棧的極限佔用,若設置過小,函數執行到一定深度會發生棧溢出。

     堆大小隻受限於計算機物理或虛擬內存,容量一般很大。只不過注意malloc失敗會返回空指針,最好做針對性處理,主動給出提示並避免內存不足時程序crash

    軟件,尤其是嵌入式軟件中,必須清楚自己的代碼佔用了多少堆/棧資源,否則,程序可能中看不中用。

分配效率

    堆管理是C函數庫實現的功能,屬於在線分配,儘管不同實現機制間效率有高有低,但總歸要佔用運行時資源,如位圖查找/鏈表查找等。而且頻繁malloc/free後往往會會造成內存碎片,需要不定時整理。如果要進行系統調用,更會引發用戶態和核心態的切換,使整體分配效率更低。

    而棧是在編譯時劃定空間,屬於離線分配,不佔用運行時資源,沒有運行時代價。

    所以單就分配效率來說,棧優於堆。

訪問效率

    有人認爲棧的訪問效率一定高於堆,這是不對的。首先要把分配與使用區分開,不能混爲一談。如上文,棧的分配效率高於堆,因爲其根本不在運行時分配。但內存的真實讀寫效率取決於其物理屬性,堆和棧具體誰的訪問效率高取決於初始化階段或線程創建階段(即系統棧/任務棧),系統把堆/棧各自定位到哪段物理內存上,包括要考慮cache的影響。

    所以如果所處物理內存相同,堆/棧的訪問效率相當。籠統說法中棧效率高於堆是綜合分配與訪問,畢竟棧沒有分配的代價。

總結

    棧由系統(編譯器)管理,不需要程序員運行時顯式分配/釋放,使用方便且整體效率高,缺點是容量一般較小,不合適大數據緩存,且棧是編譯器靜態預先使用,不如動態堆使用靈活;堆是運行時函數庫和OS提供的功能,靈活方便,適合靈活存儲動態大數據,但需要一定管理代價,且易發生堆泄漏。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章