1. 程式人生 > >SGI STL的空間配置器alloc

SGI STL的空間配置器alloc

這兩天通過閱讀SGI  STL原始碼,與《STL原始碼剖析》上侯捷對於原始碼的非常好的講解與註釋,me理解了C++關於記憶體管理的具體實現方法,覺得大有所益。。。整理一下,將實現空間配置器所用的策略大致記錄下來。

1. STL容器簡介

STL提供了很多泛型容器,如vector,list和map。程式設計師在使用這些容器時只需關心何時往容器內塞物件,而不用關心如何管理記憶體,需要用多少記憶體,這些STL容器極大地方便了C++程式的編寫。例如可以通過以下語句建立一個vector,它實際上是一個按需增長的動態陣列,其每個元素的型別為int整型:

stl::vector<int> array;
擁有這樣一個動態陣列後,使用者只需要呼叫push_back方法往裡面新增物件,而不需要考慮需要多少記憶體:
array.push_back(10);
    array.push_back(2);
vector會根據需要自動增長記憶體,在array退出其作用域時也會自動銷燬佔有的記憶體,這些對於使用者來說是透明的,stl容器巧妙的避開了繁瑣且易出錯的記憶體管理工作。

2. STL的預設記憶體分配器

隱藏在這些容器後的記憶體管理工作是通過STL提供的一個預設的allocator實現的。當然,使用者也可以定製自己的allocator,只要實現allocator模板所定義的介面方法即可,然後通過將自定義的allocator作為模板引數傳遞給STL容器,建立一個使用自定義allocator的STL容器物件,如:

stl::vector<int, UserDefinedAllocator> array;

大多數情況下,STL預設的allocator就已經足夠了。這個allocator是一個由兩級分配器構成的記憶體管理器,當申請的記憶體大小大於128byte時,就啟動第一級分配器通過malloc直接向系統的堆空間分配,如果申請的記憶體小於128byte時,就啟動第二級分配器,從一個預先分配好的記憶體池中取一塊記憶體交付給使用者,這個記憶體池由16個不同大小(8的倍數,8~128byte)的free_list組成,allocator會根據申請記憶體的大小(將這個大小round up成8的倍數)從對應的空閒塊列表取表頭塊給使用者。———————————————————————————————————————————

這裡對如何將申請記憶體大小提升至8的倍數,做一個詳細解釋:(數學證明來自網路)

公式:   ((bytes)+_ALIGN-1)&~(_ALIGN-1)

這是SGI STL 的空間配置器的第二級配置器

 template<bool threads ,int ints>

      class _default_alloc_template {...};

的私有函式

static  size_t ROUND_UP( size_t  bytes );

的具體實現。 其中 enum{ _ALIGN=8 } ;

功能是:若請求的記憶體大小bytes不是8的倍數,則上調至8的倍數,比如如果申請的是4,就會返回8;請求的是31,返回32。。

解釋:

  _ALIGN - 1 = 0b00000111

 ~(_ALIGN - 1) = 0b11111000

  ~(_ALIGN - 1)  進行 & 操作等價於去掉被8除的餘數 。所以這個等式等價於

    [(byte+7)/8]*8

就等於這個數字向上取到8的倍數。用位運算是為了增加效率。

———————————————————————————————————————————

   實現時,allocator需要維護一個儲存16個空閒塊列表表頭的陣列free_list,陣列元素i是一個指向塊大小為8*(i+1)位元組的空閒塊列表的表頭,一個指向記憶體池起始地址的指標start_free和一個指向結束地址的指標end_free。空閒塊列表節點的結構如下:

union obj {

        union obj *free_list_link;   //儲存下一個空閒塊的地址

        char client_data[1];        //這個用法比較奇異,下面做詳細解釋

};


這個共用體的位置是在一個空閒記憶體塊中的前四個位元組,當這個記憶體塊空閒時,它儲存了下個空閒塊,當這個記憶體塊交給使用者時,共用體所佔的記憶體將會被使用者資料覆蓋。因此,allocator中的空閒塊連結串列可以表示成:

obj* free_list[16];
—————————————————————————————————————————

對 共用體  obj 中的

 char client_data[1];
      做詳細解釋。

free_list_link指標儲存了下一個空閒記憶體塊的地址,按道理說不應該再有別的成員,但是事實上char client_data[1] 與這個並不衝突,它唯一可能的用處,就是被做取地址操作,得到當前記憶體塊的地址:

因為公用同一段記憶體,所以 &client_data[1]  , 便可以得到當前記憶體塊的地址~

可以說char client[1]這個陣列在第二級配置器中沒用實際意義,設計者的目的是為了構造一個柔性陣列,不是有16個大小不同的記憶體塊嗎?從1位元組-16位元組。可以這麼認為:free_list_link指標和陣列client_data[1]分別是聯合體在不同場合的代表。在free_list連結串列中,聯合體結構只應用到了free_list_link指標,作用就是連結記憶體塊,當記憶體塊從free_list連結串列中返回給使用者(動態申請)時,free_list_link將被遺棄,每個記憶體塊的前4個位元組存的就是在free_list表中該記憶體塊的下一個記憶體塊的地址,返回給使用者後,這個free_list_link將被使用者資料覆蓋。而client陣列的作用即是表示返回給使用者的記憶體空間,其實也是沒意義的,使用者根本不會用聯合體的成員訪問自己申請的記憶體空間。那到底這個client陣列有什麼作用,可以說,對使用者而言沒任何作用,但對於設計者還是一種對精煉程式碼的極致追求吧!不懂柔性陣列的可以參考:

http://www.cppblog.com/Dream5/articles/148386.html——————————————————————————————————————————

演算法描述 :

1. 分配演算法 allocate

allocator分配記憶體的演算法如下:

演算法:allocate

輸入:申請記憶體的大小size

輸出:若分配成功,則返回一個記憶體的地址,否則返回NULL

{

    if(size大於128){ 啟動第一級分配器直接呼叫malloc分配所需的記憶體並返回記憶體地址;}

    else {

        將size向上round up成8的倍數並根據大小從free_list中取對應的表頭free_list_head;

        if(free_list_head不為空){

              從該列表中取下第一個空閒塊並調整free_list;

              返回free_list_head;

        } else {

             呼叫refill演算法建立空閒塊列表並返回所需的記憶體地址;

        }

   }

}


2.演算法: refill

輸入:記憶體塊的大小size

輸出:建立空閒塊連結串列並返回第一個可用的記憶體塊地址

{

     呼叫chunk_alloc演算法分配若干個大小為size的連續記憶體區域並返回起始地址chunk和成功分配的塊數nobj;

    if(塊數為1)直接返回chunk;

    否則

    {

         開始在chunk地址塊中建立free_list;

         根據size取free_list中對應的表頭元素free_list_head;

         將free_list_head指向chunk中偏移起始地址為size的地址處, 即free_list_head=(obj*)(chunk+size);

         再將整個chunk中剩下的nobj-1個記憶體塊串聯起來構成一個空閒列表;

         返回chunk,即chunk中第一塊空閒的記憶體塊;

     }

}


演算法:chunk_alloc

輸入:記憶體塊的大小size,預分配的記憶體塊塊數nobj(以引用傳遞)

輸出:一塊連續的記憶體區域的地址和該區域內可以容納的記憶體塊的塊數

{

      計算總共所需的記憶體大小total_bytes;

      if(記憶體池中足以分配,即end_free - start_free >= total_bytes) {

          則更新start_free;

          返回舊的start_free;

      } else if(記憶體池中不夠分配nobj個記憶體塊,但至少可以分配一個){

         計算可以分配的記憶體塊數並修改nobj;

         更新start_free並返回原來的start_free;

      } else { //記憶體池連一塊記憶體塊都分配不了

         先將記憶體池的記憶體塊鏈入到對應的free_list中後;

         呼叫malloc操作重新分配記憶體池,大小為2倍的total_bytes加附加量,start_free指向返回的記憶體地址;

         if(分配不成功) {

             if(16個空閒列表中尚有空閒塊)

                嘗試將16個空閒列表中空閒塊回收到記憶體池中再呼叫chunk_alloc(size, nobj);

            else {

                   呼叫第一級分配器嘗試out of memory機制是否還有用;

            }

         }

         更新end_free為start_free+total_bytes,heap_size為2倍的total_bytes;

         呼叫chunk_alloc(size,nobj);

    }

}


演算法:deallocate

輸入:需要釋放的記憶體塊地址p和大小size

{

    if(size大於128位元組)直接呼叫free(p)釋放;

    else{

        將size向上取8的倍數,並據此獲取對應的空閒列表表頭指標free_list_head;

       調整free_list_head將p鏈入空閒列表塊中;

    }

}

—————————————————————————————————————————

小結

STL中的記憶體分配器實際上是基於空閒連結串列(free_list)的分配策略,最主要的特點是通過組織16個空閒列表,對小物件的分配做了優化。

1)小物件的快速分配和釋放。當一次性預先分配好一塊固定大小的記憶體池後,對小於128位元組的小塊記憶體分配和釋放的操作只是一些基本的指標操作,相比於直接呼叫malloc/free,開銷小。

2)避免記憶體碎片的產生。零亂的記憶體碎片不僅會浪費記憶體空間,而且會給OS的記憶體管理造成壓力。

3)儘可能最大化記憶體的利用率。當記憶體池尚有的空閒區域不足以分配所需的大小時,分配演算法會將其鏈入到對應的空閒列表中,然後會嘗試從空閒列表中尋找是否有合適大小的區域,