提起堆,大部分人都不陌生,但是其實很多人也不見得就很了解。我見過的大部分人,對堆的理解其實還停留在,全局的一種內存,速度沒有棧快,不會自動銷毀,需要開發人員自己管理。這其實不怪 Windows,怪就怪面試人員水平參差不齊,五百年了,問堆還是,堆棧究竟有什么區別。然后在中國這個應試教育橫行的地方,也必然是各種針對性的突擊,問八百個人都是上邊的答案。然而,對于 Windows 的堆,作為一個開發人員,這些了解顯然是不夠的。
其實想深入了解 Windows 中的堆,僅需要兩篇文章,日常開發就夠用了。
- Heap: Pleasures and Pains
- Managing Heap Memory
這兩篇文章,說的還算詳盡,至少基本的開發會清晰很多。
堆的使用條件
- 當程序需要的對象不能提前知曉的時候,也就是說需要在運行時動態分配的對象,要在堆上
- 棧上放不下的對象,要在堆上
堆的種類
這里要引用一段 MSDN 原文:
GlobalAlloc/GlobalFree: Heap calls that talk directly to the per-process default heap.
LocalAlloc/LocalFree: Heap calls that talk directly to the per-process default heap.
COM’s IMalloc allocator (or CoTaskMemAlloc / CoTaskMemFree): Functions use the default per-process heap. Automation uses the Component Object Model (COM)’s allocator, and the requests use the per-process heap.
C/C++ Run-time (CRT) allocator: Provides malloc() and free() as well as new and delete operators. Languages like Microsoft Visual Basic® and Java also offer new operators and use garbage collection instead of heaps. CRT creates its own private heap, which resides on top of the Windows heap.
Traditionally, the operating system and run-time libraries come with an implementation of the heap. At the beginning of a process, the OS creates a default heap called Process heap. ** The Process heap is used for allocating blocks if no other heap is used. Language run times also can create separate heaps within a process. (For example, C run time creates a heap of its own.) Besides these dedicated heaps, the application program or one of the many loaded dynamic-link libraries (DLLs) may create and use separate heaps. Windows offers a rich set API for creating and using private heaps.
從這段描述上看:
- 每個進程會有一個默認堆
- C/C++ 運行時會有自己的私有堆。
- 進程中用到的模塊,允許創建自己的私有堆。
這就非常清晰了。這也就是傳說中的一個模塊一個堆。而關于堆的種類的認知是非常必要的,因為對于堆上的內存,要本著誰申請誰釋放的原則,如果在模塊的私有堆中申請的內存,拿到模塊外由別人釋放,就會引發崩潰,因為別人釋放的時候會去自己的堆中找那部分內容,找不到就GG了。
而其實在 Windows 中關于堆分配器,其實是有前后端之分的。前端分配器維護一個固定大小的塊列表,一個內存分配過來以后先在列表中找未被使用的塊,如果找不到才會到后端分配器,新分配出一個塊,并且后端分配器還會把這個操作提交到虛擬內存。因為有前后端分配器之分,所以性能問題肯定也會在這中間產生。一個顯而易見的就是如果用到后端分配器的操作,必然會比只用前端分配器慢,所以解決這種性能問題還是盡量避免后端分配器操作。
堆的性能問題
- 內存分配
內存分配導致的慢主要還是在于當前端分配器找不到可用塊時,調用后端分配器,創建新塊,以及跟虛擬內存的交互會有性能損耗
- 內存釋放
內存釋放導致的慢是由于釋放內存會有一個塊合并的操作,將空閑塊合并到一起重組成一個大的空閑塊,但是這中間會引發對內存的無序訪問,導致緩沖命中失敗和性能下降。
- 堆競爭
在多線程的情況,出現多個線程訪問一個堆,需要有一個等待過程。而加鎖,會引起線程的上下文切換也是性能下降的原因之一。
- 堆破壞
程序沒有正確使用堆導致對破壞
- 頻繁的 alloc 和 realloc
腳本語言容易發生,不過現在的腳本語言解釋器都比較機智了,都會分配一塊很大的內存自己用,來避免這個。
提升堆性能的一般操作
- 避免使用指針關聯兩個數據結構
使用指針關聯兩個數據結構會導致對象的分配和釋放被分離,產生額外開銷。
- 把孩子對象嵌入父親對象。
減少額外分配內存的次數。
- 合并小對象組成一個大對象(聚合)
可以減少被分配的塊的數量來提升性能,關鍵是要找好聚合邊界
- 用 Buffer 滿足 80% 的需求(二八原則)
用內存 Buffer 存儲字符串或者二進制數據,開一個能滿足 80% 需求的大小的 Buffer 即可。剩下 20% 可以開一個新的 Buffer,然后持有指針即可。這樣可以減少內存分配和釋放,也可以減少數據空間,會提升性能
- 成塊分配內存對象
小聲BB(我個人理解就是指內存池)
- 使用
_amblksizC語言運行時(CRT)特有的前端分配器,可以用它跟后端分配器申請分配一個比較大的塊,從而減少對后端分配器的請求。
提升堆性能的進階操作
- 使用 Windows Heap
- 使用內存池
- 使用 MP Heap。(一個多進程友好的包)
- 重新思考算法與數據結構
改善堆性能之前需要做的
- 評估代碼中堆的使用方法
- 梳理代碼,減少關于堆的調用,修復錯誤并調整數據結構
- 要對堆的性能消耗做具體評估
總結
很多人會認為這些過于底層,對一般開發用處不大,但其實對堆的深入了解,除了可以在程序性能上有一些更大的提升,對于一般開發則可以寫出質量更高的代碼。只有對操作系統的了解足夠的深入,才能寫出跟操作系統有著完美配合的代碼,這看似是一種玄學,其實是一種科學。是基于 Windows 平臺開發應用的開發人員與操作系統的開發人員的一種默契。