一些關於WINDOWS下堆棧知識的集合

來源:互聯網
上載者:User
堆棧基本知識堆(heap)和棧(stack)是C/C++編程不可避免會碰到的兩個基本概念。首先,這兩個概念都可以在講資料結構的書中找到,他們都是基本的資料結構,雖然棧更為簡單一些。在具體的C/C++編程架構中,這兩個概念並不是並行的。對底層機器代碼的研究可以揭示,棧是機器系統提供的資料結構,而堆則是C/C++函數庫提供的。具體地說,現代電腦(串列執行機制),都直接在代碼底層支援棧的資料結構。這體現在,有專門的寄存器指向棧所在的地址,有專門的機器指令完成資料入棧出棧的操作。這種機制的特點是效率高,支援的資料有限,一般是整數,指標,浮點數等系統直接支援的資料類型,並不直接支援其他的資料結構。 因為棧的這種特點,對棧的使用在程式中是非常頻繁的。對子程式的調用就是直接利用棧完成的。機器的call指令裡隱含了把返回地址推入棧,然後跳轉至子程式地址的操作,而子程式中的ret指令則隱含從堆棧中彈出返回地址並跳轉之的操作。C/C++中的自動變數是直接利用棧的例子,這也就是為什麼當函數返回時,該函數的自動變數自動失效的原因。 和棧不同,堆的資料結構並不是由系統(無論是機器系統還是作業系統)支援的,而是由函數庫提供的。基本的malloc/realloc/free函數維護了一套內部的堆資料結構。當程式使用這些函數去獲得新的記憶體空間時,這套函數首先試圖從內部堆中尋找可用的記憶體空間,如果沒有可以使用的記憶體空間,則試圖利用系統調用來動態增加程式資料區段的記憶體大小,新分配得到的空間首先被組織進內部堆中去,然後再以適當的形式返回給調用者。當程式釋放分配的記憶體空間時,這片記憶體空間被返回內部堆結構中,可能會被適當的處理(比如和其他空閑空間合并成更大的空閑空間),以更適合下一次記憶體配置申請。這套複雜的分配機制實際上相當於一個記憶體配置的緩衝池(Cache),使用這套機制有如下若干原因:1. 系統調用可能不支援任意大小的記憶體配置。有些系統的系統調用只支援固定大小及其倍數的記憶體請求(按頁分配);這樣的話對於大量的小記憶體分類來說會造成浪費。2. 系統調用申請記憶體可能是代價昂貴的。系統調用可能涉及使用者態和核心態的轉換。3. 沒有管理的記憶體配置在大量複雜記憶體的分配釋放操作下很容易造成記憶體片段。 堆和棧的對比從以上知識可知,棧是系統提供的功能,特點是快速高效,缺點是有限制,資料不靈活;而堆是函數庫提供的功能,特點是靈活方便,資料適應面廣泛,但是效率有一定降低。棧是系統資料結構,對於進程/線程是唯一的;堆是函數庫內部資料結構,不一定唯一。不同堆分配的記憶體無法互相操作。棧空間分靜態分配和動態分配兩種。靜態分配是編譯器完成的,比如自動變數(auto)的分配。動態分配由malloc函數完成。棧的動態分配無需釋放(是自動的),也就沒有釋放函數。為可移植的程式起見,棧的動態分配操作是不被鼓勵的!堆空間的分配總是動態,雖然程式結束時所有的資料空間都會被釋放回系統,但是精確的申請記憶體/釋放記憶體匹配是良好程式的基本要素。  使用堆棧 1 堆實現的注意事項傳統上,作業系統和執行階段程式庫是與堆的實現共存的。在一個進程的開始,作業系統建立一個預設堆,叫做“進程堆”。如果沒有其他堆可使用,則塊的分配使用“進程堆”。語言運行時也能在進程內建立單獨的堆。(例如,c 運行時建立它自己的堆。)除這些專用的堆外,應用程式或許多已載入的動態連結程式庫 (dll) 之一可以建立和使用單獨的堆。win32提供一整套 api 來建立和使用私人堆。有關堆函數(英文)的詳盡指導,請參見 msdn。當應用程式或 dll 建立私人堆時,這些堆存在於進程空間,並且在進程內是可訪問的。從給定堆分配的資料將在同一個堆上釋放。(不能從一個堆分配而在另一個堆釋放。)在所有虛擬記憶體系統中,堆駐留在作業系統的“虛擬記憶體管理器”的頂部。語言運行時堆也駐留在虛擬記憶體頂部。某些情況下,這些堆是作業系統堆中的層,而語言運行時堆則通過大塊的分配來執行自己的記憶體管理。不使用作業系統堆,而使用虛擬記憶體函數更利於堆的分配和塊的使用。 典型的堆實現由前、後端分配程式組成。前端分配程式維持固定大小塊的空閑列表。對於一次分配調用,堆嘗試從前端列表找到一個自由塊。如果失敗,堆被迫從後端(保留和提交虛擬記憶體)分配一個大塊來滿足請求。通用的實現有每塊分配的開銷,這將耗費執行循環,也減少了可使用的儲存空間。 knowledge base 文章 q10758,“用 calloc() 和 malloc() 管理記憶體” (搜尋文章編號), 包含了有關這些主題的更多背景知識。另外,有關堆實現和設計的詳細討論也可在下列著作中找到:“dynamic storage allocation: asurvey and critical review”,作者 paul r. wilson、mark s. johnstone、michaelneely 和 david boles;“international workshop on memory management”, 作者 kinross, scotland, uk, 1995 年 9 月(http://www.cs.utexas.edu/users/oops/papers.html)(英文)。windows nt 的實現(windows nt 版本 4.0 和更新版本) 使用了 127 個大小從 8 到 1,024 位元組的 8 位元組對齊塊空閑列表和一個“大塊”列表。“大塊”列表(空閑列表[0]) 儲存大於 1,024 位元組的塊。空閑列表容納了用雙向鏈錶鏈接在一起的對象。預設情況下,“進程堆”執行收集操作。(收集是將相鄰空閑塊合并成一個大塊的操作。)收集耗費了額外的周期,但減少了堆塊的內部片段。 單一全域鎖保護堆,防止多線程式的使用。(請參見“server performance and scalability killers”中的第一個注意事項, george reilly 所著,在 “msdn online web workshop”上(網站:http://msdn.microsoft.com/workshop/server/iis/tencom.asp(英文)。)單一全域鎖本質上是用來保護堆資料結構,防止跨多線程的隨機存取。若堆操作太頻繁,單一全域鎖會對效能有不利的影響。 2 什麼是常見的堆效能問題?以下是您使用堆時會遇到的最常見問題: 分配操作造成的速度減慢。光分配就耗費很長時間。最可能導致運行速度減慢原因是空閑列表沒有塊,所以運行時分配程式碼會耗費周期尋找較大的空閑塊,或從後端分配程式分配新塊。釋放操作造成的速度減慢。釋放操作耗費較多周期,主要是啟用了收集操作。收集期間,每個釋放操作“尋找”它的相鄰塊,取出它們並構造成較大塊,然後再把此較大塊插入空閑列表。在尋找期間,記憶體可能會隨機碰到,從而導致快取不能命中,效能降低。 堆競爭造成的速度減慢。當兩個或多個線程同時訪問資料,而且一個線程繼續進行之前必須等待另一個線程完成時就發生競爭。競爭總是導致麻煩;這也是目前多處理器系統遇到的最大問題。當大量使用記憶體塊的應用程式或 dll 以多線程方式運行(或運行於多處理器系統上)時將導致速度減慢。單一鎖定的使用—常用的解決方案—意味著使用堆的所有操作是序列化的。當等待鎖定時序列化會引起線程切換上下文。可以想象交叉路口閃爍的紅燈處走走停停導致的速度減慢。競爭通常會導致線程和進程的環境切換。環境切換的開銷是很大的,但開銷更大的是資料從處理器快取中丟失,以及後來線程複活時的資料重建。 堆破壞造成的速度減慢。造成堆破壞的原因是應用程式對堆塊的不正確使用。通常情形包括釋放已釋放的堆塊或使用已釋放的堆塊,以及塊的越界重寫等明顯問題。(破壞不在本文討論範圍之內。有關記憶體重寫和泄漏等其他細節,請參見 microsoft visual c++(r) 調試文檔 。)頻繁的分配和重分配造成的速度減慢。這是使用指令碼語言時非常普遍的現象。如字串被反覆分配,隨重分配增長和釋放。不要這樣做,如果可能,盡量分配大字串和使用緩衝區。另一種方法就是盡量少用串連操作。競爭是在分配和釋放操作中導致速度減慢的問題。理想情況下,希望使用沒有競爭和快速分配/釋放的堆。可惜,現在還沒有這樣的通用堆,也許將來會有。 在所有的伺服器系統中(如 IIS、msproxy、databasestacks、網路伺服器、 exchange ?其他), 堆鎖定實在是個大瓶頸。處理器數越多,競爭就越會惡化。 盡量減少堆的使用現在您明白使用堆時存在的問題了,難道您不想擁有能解決這些問題的超級魔棒嗎?我可希望有。但沒有魔法能使堆運行加快—因此不要期望在產品出貨之前的最後一星期能夠大為改觀。如果提前規劃堆策略,情況將會大大好轉。調整使用堆的方法,減少對堆的操作是提高效能的良方。如何減少使用堆操作?通過利用資料結構內的位置可減少堆操作的次數。請考慮下列執行個體:struct objecta {// objecta 的資料}struct objectb {// objectb 的資料}// 同時使用 objecta 和 objectb//// 使用指標//struct objectb {struct objecta * pobja;// objectb 的資料} //// 使用嵌入//struct objectb {struct objecta pobja;// objectb 的資料}//// 集合 – 在另一對象內使用 objecta 和 objectb//struct objectx {struct objecta obja;struct objectb objb;}避免使用指標關聯兩個資料結構。如果使用指標關聯兩個資料結構,前面執行個體中的對象 a 和 b 將被分別分配和釋放。這會增加額外開銷—我們要避免這種做法。把帶指標的子物件嵌入父物件。當對象中有指標時,則意味著對象中有動態元素(百分之八十)和沒有引用的新位置。嵌入增加了位置從而減少了進一步分配/釋放的需求。這將提高應用程式的效能。 合并小對象形成大對象(彙總)。彙總減少分配和釋放的塊的數量。如果有幾個開發人員,各自開發設計的不同部分,則最終會有許多小對象需要合并。整合的挑戰就是要找到正確的彙總邊界。 內聯緩衝區能夠滿足百分之八十的需要(aka 80-20 規則)。個別情況下,需要記憶體緩衝來儲存字串/位元據,但事先不知道總位元組數。估計並內聯一個大小能滿足百分之八十需要的緩衝區。對剩餘的百分之二十,可以分配一個新的緩衝區和指向這個緩衝區的指標。這樣,就減少分配和釋放調用並增加資料的位置空間,從根本上提高代碼的效能。在塊中指派至(塊化)。塊化是以組的方式一次分配多個對象的方法。如果對列表的項連續跟蹤,例如對一個 {名稱,值} 對的列表,有兩種選擇:選擇一是為每一個“名稱-值”對分配一個節點;選擇二是分配一個能容納(如五個)“名稱-值”對的結構。例如,一般情況下,如果儲存四對,就可減少節點的數量,如果需要額外的空間數量,則使用附加的鏈表指標。塊化是友好的處理器快取,特別是對於 l1-快取,因為它提供了增加的位置 —不用說對於塊分配,很多資料區塊會在同一個虛擬頁中。 正確使用 _amblksiz。c 運行時 (crt) 有它的自訂前端分配程式,該分配程式從後端(win32 堆)分配大小為 _amblksiz 的塊。將 _amblksiz 設定為較高的值能潛在地減少對後端的調用次數。這隻對廣泛使用 crt 的程式適用。使用上述技術將獲得的好處會因物件類型、大小及工作量而有所不同。但總能在效能和可升縮性方面有所收穫。另一方面,代碼會有點特殊,但如果經過深思熟慮,代碼還是很容易管理的。 3 其他提高效能的技術下面是一些提高速度的技術:使用 windows nt5 堆由於幾個同事的努力和辛勤工作,1998 年初 microsoft windows(r) 2000 中有了幾個重大改進:改進了堆代碼內的鎖定。堆代碼對每堆一個鎖。全域鎖保護堆資料結構,防止多線程式的使用。但不幸的是,在高通訊量的情況下,堆仍受困於全域鎖,導致高競爭和低效能。windows 2000 中,鎖內代碼的臨界區將競爭的可能性減到最小,從而提高了延展性。使用“lookaside”列表。堆資料結構對塊的所有空閑項使用了大小在 8 到 1,024 位元組(以 8-位元組遞增)的快速快取。快速快取最初保護在全域鎖內。現在,使用 lookaside 列表來訪問這些快速快取空閑列表。這些列表不要求鎖定,而是使用 64 位元的互鎖操作,因此提高了效能。內部資料結構演算法也得到改進。這些改進避免了對分配快取的需求,但不排除其他的最佳化。使用 windows nt5 堆評估您的代碼;它對小於 1,024 位元組 (1 kb) 的塊(來自前端分配程式的塊)是最佳的。globalalloc() 和 localalloc() 建立在同一堆上,是存取每個進程堆的通用機制。如果希望獲得高的局部效能,則使用 heap(r) api 來存取每個進程堆,或為分配操作建立自己的堆。如果需要對大塊操作,也可以直接使用 virtualalloc() / virtualfree() 操作。 上述改進已在 windows 2000 beta 2 和 windows nt 4.0 sp4 中使用。改進後,堆鎖的競爭率顯著降低。這使所有 win32 堆的直接使用者受益。crt 堆建立於 win32 堆的頂部,但它使用自己的小塊堆,因而不能從 windows nt 改進中受益。(visual c++ 版本 6.0 也有改進的堆分配程式。) 使用分配快取分配快取允許快取分配的塊,以便將來重用。這能夠減少對進程堆(或全域堆)的分配/釋放調用的次數,也允許最大限度的重用曾經分配的塊。另外,分配快取允許收集統計資訊,以便較好地理解對象在較高層次上的使用。 典型地,自訂堆分配程式在進程堆的頂部實現。自訂堆分配程式與系統堆的行為很相似。主要的差別是它在進程堆的頂部為分配的對象提供快取。快取設計成一套固定大小(如 32 位元組、64 位元組、128 位元組等)。這一個很好的策略,但這種自訂堆分配程式丟失與分配和釋放的對象相關的“語義資訊”。與自訂堆分配程式相反,“分配快取”作為每類分配快取來實現。除能夠提供自訂堆分配程式的所有好處之外,它們還能夠保留大量語義資訊。每個分配快取處理常式與一個目標二進位對象關聯。它能夠使用一套參數進行初始化,這些參數表示並發層級、對象大小和保持在空閑列表中的元素的數量等。分配快取處理常式對象維持自己的私人空閑實體集區(不超過指定的閥值)並使用私人保護鎖。合在一起,分配快取和私人鎖減少了與主系統堆的通訊量,因而提供了增加的並發、最大限度的重用和較高的延展性。 需要使用清理程式來定期檢查所有分配快取處理常式的活動情況並回收未用的資源。如果發現沒有活動,將釋放指派至的池,從而提高效能。可以審核每個分配/釋放活動。第一級資訊包括對象、分配和釋放調用的總數。通過查看它們的統計資訊可以得出各個對象之間的語義關係。利用以上介紹的許多技術之一,這種關係可以用來減少記憶體配置。 分配快取也起到了調試助手的作用,協助您跟蹤沒有完全清除的對象數量。通過查看態堆棧返回蹤跡和除沒有清除的對象之外的簽名,甚至能夠找到確切的失敗的調用者。mp 堆mp 堆是對多處理器友好的分布式分配的程式包,在 win32 sdk(windows nt 4.0 和更新版本)中可以得到。最初由 jvert 實現,此處堆抽象建立在 win32 堆程式包的頂部。mp 堆建立多個 win32 堆,並試圖將分配調用分布到不同堆,以減少在所有單一鎖上的競爭。本程式包是好的步驟 —一種改進的 mp-友好的自訂堆分配程式。但是,它不提供語義資訊和缺乏統計功能。通常將 mp 堆作為 sdk 庫來使用。如果使用這個 sdk 建立可重用組件,您將大大受益。但是,如果在每個 dll 中建立這個 sdk 庫,將增加工作設定。重新思考演算法和資料結構要在多處理器機器上伸縮,則演算法、實現、資料結構和硬體必須動態伸縮。請看最經常分配和釋放的資料結構。試問,“我能用不同的資料結構完成此工作嗎?”例如,如果在應用程式初始化時載入了唯讀項的列表,這個列表不必是線性連結的列表。如果是動態分配的數組就非常好。動態分配的數組將減少記憶體中的堆塊和片段,從而增強效能。減少需要的小對象的數量減少堆分配程式的負載。例如,我們在伺服器的關鍵處理路徑上使用五個不同的對象,每個對象單獨分配和釋放。一起快取這些對象,把堆調用從五個減少到一個,顯著減少了堆的負載,特別當每秒鐘處理 1,000 個以上的請求時。如果大量使用“automation”結構,請考慮從主線代碼中刪除“automation bstr”,或至少避免重複的 bstr 操作。(bstr 串連導致過多的重分配和分配/釋放操作。) 摘要對所有平台往往都存在堆實現,因此有巨大的開銷。每個單獨代碼都有特定的要求,但設計能採用本文討論的基本理論來減少堆之間的相互作用。1.評價您的代碼中堆的使用。2.改進您的代碼,以使用較少的堆調用:分析關鍵路徑和固定資料結構。3.在實現自訂的封裝程式之前使用量化堆調用成本的方法。4.如果對效能不滿意,請要求 os 組改進堆。更多這類請求意味著對改進堆的更多關注。5.要求 c 運行時組針對 os 所提供的堆製作小巧的分配封裝程式。隨著 os 堆的改進,c運行時堆調用的成本將減小。作業系統(windows nt 家族)正在不斷改進堆。請隨時關注和利用這些改進.
相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.