內存分配(1) — 空閒鏈表

內存分配是所有成功的庫都要費大量心力去做好的事情,除非是對performance很高的需求,至少我現在在工作中很少需要自己來寫內存分配策略。我始終覺得一些經典的庫,像STL,Loki,Boost是最好的教材。讀書也要講究方法,讀STL這樣的庫,我現在會逼着自己多問幾個爲什麼,爲什麼作者要這樣設計呢,如果換作我自己,我會怎樣設計呢,有什麼地方沒有考慮周全呢。今天我試圖來說說內存分配,希望大家多發表意見。交流才能進步嘛-:)
我們經常會寫
CMyObject* pObj = new CMyObject(…);  // 從堆上分配一個對象
其實編譯器會產生下面的代碼:
CMyObject* pObj;
try{
    void* mem = ::operator new( sizeof(CMyObject) ); // 分配內存
    pObj = static_cast<CMyObject*>(mem);
    pc->CMyObject::CMyObject(…);
}
catch( std::bad_alloc)
{
   // 如果內存分配失敗就不執行constructor
}
可見,在C++中簡單的new一個對象,實際上是兩個操作:1)分配內存,內存大小用sizeof在編譯時就計算出來了 2)在分配得到的內存地址上調用構造函數。 我們今天討論的主要是集中在第一步。
可以把::operator new()看作是malloc,其實這最終會導致一個系統調用,去向系統要資源。在文件I/O一樣,要提高讀寫速度,關鍵是要減少物理文件I/O的次數(磁盤尋道,從物理磁盤讀寫),典型的解決方案就是buffer,每次我都多要一點,下次要讀得東西如果在buffer裏面,就不需要從磁盤拿了,畢竟內存操作要比磁盤操作快太多太多了。那自然就想到了,在內存分配中也一樣啊,要減少客戶程序實際向系統要內存的次數,我每次要得時候多要一點不就可以了嗎。是的,那在C++裏面怎麼實現呢?
在實現的時候有一點要求,就是分配程序的改動,因該盡最大可能不影響客戶程序代碼,最好一點影響也沒有 .這裏我們可以通過重載類成員函數operator new()和operator delete來做:
我的想法是這樣的:
在第一次需要分配一個CMyObject對象的時候,我就分配100個sizeof(CMyObject)大小的內存,以後第二次要分配一個CMyObject對象的時候,我就從上次剩下的99個後備中間拿一個出來。爲了達到這樣的目標,我可以維護一個單向鏈表,一開始把這100個對象串起來(這需要定義一個成員變量next),還需要一個指針,指向下一個可供使用的對象(freeStore)。
class CMyObject{
public:
        void* operator new(size_t);
        void  operator delete(void*,size_t);
private:
        CMyObject* next;
        static CMyObject* freeStore;  //注意阿,freeStore是靜態類成員,給所用CMyObject的實例共享哦
};
CMyObject* CMyObject::freeStore = NULL;
 
void* CMyObject::operator new(size_t size)
{
     CMyObject* p = NULL;
     if(!freeStore){ // 第一次要,就要多一點
          size_t chunk = 100*size; // 說句良心話,要他個100個對系統來說也不算多
          freeStore = reinterpret_cast<CMyObject*>(new char[chunk]);  
          // 然後把內存用next指針串起來,物理上是一段連續內存,邏輯上是一個空閒對象的單向鏈表
         for(p = freeStore; p != &freeStore[100-1]; ++p) p->next = p+1;
         p->next = NULL;
     }
     // 從庫存中拿出第一個,給請求的人
     p = freeStore;
     freeStore = freeStore->next; // 調整freeStore到下一個空閒對象
     return p;    
 }
 
下面我們看看釋放:
void CMyObject::operator delete(void* p,size_t)
{
      // 把要delete的對象,放到free鏈表的頭部,並不歸還給系統哦!
     (static_cast<CMyObject*>(p)) ->next = freeStore;
     freeStore = static_cast<CMyObject*>(p);
}
整個過程就是這樣的,其實可以看成一個邏輯上的stack,先reserve一下stack的大小爲100,new的時候pop一個出來,delete的時候push回去。
 
有兩個地方要注意了:
1. 這是個欠債不還錢的主哦,問系統拿的東西,不還的。要到程序退出的時候,才由進程負責清理。
2. 每次向系統索要內存時,其實都會都要一點點用於記錄這塊內存的大小之類的,我猜至少是4個字節,但是現在每個類都多了一個額外的next指針的代價。似乎除了分配速度提高以外,在大小上沒有優勢。我們馬上會談到如何使用embedded pointer解決這個問題。


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