比較記憶體配置方法

原文出自: Comparing Memory Allocation Methods

GlobalAlloc/LocalAlloc/HeapAlloc 有點類似。GlobalAlloc/LocalAlloc 底層也是呼叫 HeapAlloc。不同點是,HeapAlloc 能夠丟出 exception (比如無法配置所需空間),但 LocalAlloc 不行。而 LocalAlloc 能夠回傳 memory object 的 handle,如果我們需要 reallocate 記憶體時,就不用重新產生新的 handle。

VirtualAlloc 允許對配置區域設定更多選項。不過它最小配置單位是 page,一般來說是 4K --- 這會比較佔用記憶體,如果我們用不到這麼多的話。相反的,HeapAlloc 則是以 heap 為單位,一般來說是 8 bytes。

而 malloc 是 c run-time library,是 run-time dependent。new 則是 compiler dependent 與 language dependent。

以下取自程式設計俱樂部的討論

在Microsoft Visual Studio\VC98\CRT\SRC裡有MFC的source code可參考.

new.cpp: 可以看到轉呼叫到_nh_malloc, 實際呼叫到_nh_malloc_base
malloc.c: _malloc_base就是malloc函數呼叫的函數, 實際也是呼叫到_nh_malloc_base

_nh_malloc_base主要是限制大小, 以及exception handle處理, 主要是轉呼叫到_heap_alloc_base函數.

_heap_alloc_base 裡會依__active_heap是__V6_HEAP, __V5_HEAP, __SYSTEM_HEAP來決定處理方式. __V6_HEAP為VC++ 6.0自行管理方式, __V5_HEAP為VC++ 5.0自行管理方式, __SYSTEM_HEAP則直接轉呼叫HeapAlloc函數

heapinit.c: __heap_select函數決定__active_heap的值. 一開始先判斷OS是否為NT 5.0以上, 是的話便是__SYSTEM_HEAP. 其後再依環境變數與連結的dll版本來判斷是__V6_HEAP還是__V5_HEAP.

---

VC++的new/malloc在NT 5.0以上和HeapAlloc是一樣的, 不過new/malloc的速度稍慢些 (因為轉呼叫了好幾層). 但這點速度差, 其實是微不足道的, 為了容易維護, 使用new/malloc還是比較好些.

在NT 5.0以下的版本, VC++是自己管理. 我記得曾在某個網頁看到說, 使用HeapAlloc的額外使用記憶體較少, 但這點我是存疑的. 因為從VC++的source code裡, 可以明確得知每個區塊會多使用24 byte記憶體 (16 byte MCB + 8 byte heap管理), 但HeapAlloc應該也是有使用額外記憶體, 只是無法得知其大小 (沒有相關的資料). 因此實際使用上, 也未必HeapAlloc便優於new/malloc. 假設HeapAlloc額外使用的記憶體比new/malloc少, 那麼只有在使用極多個動態記憶體區塊的情況下, 才能顯現優點 (實際使用的總記憶體量較少). 不過這種情況下, 速度一定都很慢, 因為動態記憶體的配置與釋放, 是一個很慢的運算. 真的要求速度與記憶體用量, 應該直接取得一大塊記憶體, 然後自行管理, 才是最佳做法. 這麼一來, 用new/malloc跟用HeapAlloc也就沒什麼差了.

---

在Windows的記憶體管理函數裡, 以GlobalAlloc與GlobalFree比較常用, 而且通常是用在COM元件上, 這部份請自己去看COM方面的書. Raymond提的兩篇, 個人認為助益不大, 真的想要了解的話, 便去將MSDN裡的"Memory Management"這個章節, 從頭到尾好好的看一遍, 才能有個較完整的概念. 不然東看一點, 西看一點, 終究還只是似懂非懂的而已.

不過這只是Windows的記憶體管理方式, 其他作業系統又不一樣了. 在寫程式時, 應該儘量避開這類問題. 就如我所說的: "真的要求速度與記憶體用量, 應該直接取得一大塊記憶體, 然後自行管理". 其實在"排序演算法專論"裡, 我已提了好幾次, 這邊再來說一次.

今天要寫一個鏈結串列, 我想絕大部份的人都會想到用下列結構:

class Node
{
public:
    int m_Data; // 資料 //
    Node* m_Next; // 指向下個節點 //
};

在學校教的, 其實也是這樣的結構, 主要是好理解. 但在實作上卻不一定如此. 假設今天的資料量有100M個, 那麼就要有100M個動態記憶體區塊來當節點, 表面上是用了800MB記憶體, 但以Windows而言, 額外記憶體就高達2.4GB! 再就速度而言, alloc與free 100M個動態記憶體需費時多久? 至少十幾秒到幾十秒 (不相信自己試試). 如果講求效率的軟體, 絕對不會這樣做. 如果改用陣列來模擬串列 (參見我寫的"資料結構應用篇"):

no = 100M
int* m_Data = new int[no];
int* m_Next = new int[no]; // 指向下個節點的索引值,可用-1做為節點結束 //

這樣額外記憶體只有48 byte, alloc/free次數也只有2次, 無論系統是如何管理記憶體的, 幾乎沒什麼太大影響, 而且無論記憶體使用量與速度而言, 都是前者無法比擬的. 不同的資料形態與資料量, 採用的資料結構與演算法都不太一樣, 這部份是實戰經驗, 理論上很難學到的, 要前輩教也教不來, 因為沒有定式. 只能多看(高手的程式), 多想, 多實驗, 來得到一個最佳的處理方式. 我能做的, 也只是將我曾碰到過的案例, 整理出較通用的法則提供給大家參考, 但實際實作時, 還是得視實際情況而做某種程度的調整. 就像我看open ssl裡對於DES的加解密寫法, 真的令人嘆為觀止. 理論難, 實作也難, 並不是大家想要怎麼寫就怎麼寫.

留言

熱門文章