SGI STL中內存池的實現

最近這兩天研究了一下SGI STL中的內存池, 網上對於這一塊的講解不少, 可是要麼講的不完整, 要麼講的不夠簡單(至少對於我這樣的初學者來說是這樣的...), 因此接下來我將把我對於對於SGI STL的理解寫下來, 方便之後查閱同時也但願可以對像我同樣剛剛接觸C++的初學者提供一些幫助吧.node

 

首先咱們須要明確, 內存池的目的究竟是什麼?  首先你要知道的是, 咱們每次使用new T來初始化類型T的時候, 其實發生了兩步操做, 一個叫內存分配, 這一步使用的其實不是new而是operator new(也能夠認爲就是C語言中的malloc), 這一步是直接和操做系統打交道的, 操做系統可能須要通過相對繁瑣的過程才能將一塊指向空閒內存的指針返回給用戶, 因此這也是new比較耗時的一部分, 而第二步就是使用構造函數初始化該內存, 這是咱們比較熟悉的. 既然內存分配耗時, 那咱們很容易想到的就是一次性分配一大塊內存, 而後在用戶須要的時候再劃分其中一部分給用戶, 這樣的話, 一次分配, 屢次使用, 天然而然提升了效率, 而用來管理這所謂的一大塊內存的數據結構, 也就是今天咱們要說的內存池. 另一個好處在於, 頻繁地使用new將致使系統內存空間碎片化嚴重, 容易致使的後果就是很難找到一塊連續的大塊內存, 空間利用率低.程序員

 

那麼咱們先來看看, 內存池的總體結構 :數組

 

 該內存池能夠認爲由上面的一個指針數組和下面的自由鏈表兩部分組成, 指針數組中第一個指針指向的是存放內存大小爲8bytes的節點串接而成的自由鏈表, 以後依次是內存而16bytes, 24bytes直到128bytes, 固然在圖中我只畫出了一個自由鏈表. 因此內存池的基本思路在於 :數據結構

1. 若是用戶分配的內存大於128bytes, 直接用malloc, 不然的話找出適合的自由鏈表, 從其上摘下一個節點將其頭指針返回給用戶.函數

2. 釋放過程則正好與分配相對應, 若是用戶分配的內存大於128bytes, 直接用free, 不然找出適當的自由鏈表, 將指針所指的該段內存從新鏈接到自由鏈表中(注意此時並不返回給操做系統, 由於以後還能夠再重複利用). ui

這一部分的所對應的代碼以下圖 :this

 1 private:
 2     static const int Align = 8;
 3     static const int MaxBytes = 128;
 4     static const int NumberOfFreeLists = MaxBytes / Align;
 5     static const int NumberOfAddedNodesForEachTime = 20;
 6 
 7     union node {
 8         union node *next;
 9         char client[1];
10     };
11 
12     static obj *freeLists[NumberOfFreeLists];

 

爲了便於理解, 我對於源代碼中的因此屬性名都作了相應的改動, 惟一可能存在疑問的是這個node爲何能夠用聯合體?  這裏咱們須要搞清楚這麼幾點, 自由鏈表上保存的都是一個一個並未使用的節點, 此時咱們爲了將全部的node串接起來, 咱們固然能夠獨立分配空間來實現這一功能, 以下圖, 比較容易想到的作法多是這樣, 用一個結構體來維護指向真正要分配給用戶的內存塊以及下一個結構體. 可是這樣作有兩個缺點 :spa

1.首先它的每個node都須要額外多出一個指針的空間來保存真正要分配給用戶的內存塊的地址操作系統

2. 其次在將該內存塊分配出去以後, 還須要再處理掉該node對應的結構體.指針

在分析分配函數的代碼以前, 咱們先來看看幾個輔助函數 :

1 private:
2     static size_t ROUND_UP(size_t size) {
3         return ((size + Align - 1) & ~(Align - 1));
4     }
5 
6     static size_t FREELIST_INDEX(size_t size) {
7         return (size + Align - 1) / Align - 1;
8     }

這兩個函數做用很簡單, 第一個返回的是大於等於輸入值的8的倍數, 第二個返回的是能夠容納輸入值的最小的自由鏈表.

 

接下來就是內存池對外的接口, allocate函數的實現代碼.

 1 void* alloc::allocate(size_t size) {
 2     if (size > MaxBytes) {
 3         return malloc(size);
 4     }
 5 
 6     size_t index = FREELIST_INDEX(size);
 7     node* theMostSuitableNode = freeLists[index];
 8     if (theMostSuitableNode) {
 9         freeLists[index] = theMostSuitableNode->next;
10         return theMostSuitableNode;
11     }
12     else {
13         return refill(ROUND_UP(size));
14     }
15 }

1. 正如咱們前面所講的, 當用戶但願獲得size大小的內存空間時候, 此時咱們只須要找到可以容納size的最小的自由鏈表, 由於自由鏈表中都是還未分配出去的空間, 若是自由鏈表中還存在節點的話, 直接將該節點分配出去便可, 也就是這裏的theMostSuitableNode不爲空的狀況, 但此時咱們要將數組中指向該自由鏈表的指針指向下一個Node, 由於這個Node已經分配出去了.

2. 另外一方面, 若是自由鏈表中並無可用的Node(這裏有兩種狀況會致使沒有可用的Node, 第一種是曾經分配過, 可是用光了, 第二種是這是該內存池初始化以來第一次使用這個大小的自由鏈表, 因此還未分配過空間), 咱們直接使用refill函數來填充自由鏈表, 之因此要用ROUND_UP使得它成爲8的倍數, 是由於處於效率緣由咱們可能會一次性分配不止1個Node(這裏是20個), 因此這裏的空間必須按照Node的大小來分配.

 

因此咱們順蔓摸瓜, 接着來看refill的實現代碼.

 1 void* alloc::refill(size_t size) {
 2     size_t num = NumberOfAddedNodesForEachTime;
 3     char* block = blockAlloc(size, num);
 4     node** currentFreeList = 0;
 5     node *curNode = 0, *nextNode = 0;
 6 
 7     if (num == 1) {
 8         return block;
 9     }
10     else {
11         currentFreeList = freeLists + FREELIST_INDEX(size);
12         *currentFreeList = nextNode = reinterpret_cast<node*>(block + size);
13         for (int i = 1;; ++i) {
14             curNode = nextNode;
15             nextNode = reinterpret_cast<node*>(reinterpret_cast<char*>(curNode) + size);
16             if (num - 1 == i) {
17                 curNode->next = 0;
18                 break;
19             }
20             else {
21                 curNode->next = nextNode;
22             }
23         }
24         return block;
25     }
26 }

先解釋一下第二行的blockAlloc, 這個函數的做用是去內存池中尋找size * num大小的空間而後劃分給當前的自由鏈表(也就是currentFreeList), 由於一旦調用了refill說明該自由鏈表已經沒有了可分配的Node, 因此咱們這裏考慮再分配的時候就直接分配了NumberOfAddedNodesForEachTime個(也就是20個). 可是要注意的話其實這裏num傳進去的是引用, 爲何傳引用呢? 由於還有可能會出現內存池空間不夠的狀況, 此時若是內存池夠1個Node可是不夠20個的話, 就會將num設置爲1, 說明此時只分配了1個Node空間. 因此能夠看到第26行的判斷中, 當num爲1的時候, 直接將block返回給用戶便可. 若是不是1的話, 再返回以前要先將剩下個節點串接在自由鏈表上. 這也就是那個for循環的做用.

 

固然在接觸到blockAlloc以前, 咱們先來看內存池的另外另個熟悉.

1 static char *startOfPool, *endOfPool;

這兩個變量分別指向內存池所分配的空間中的起點和終點, 以前說道自由鏈表裏面若是沒有node了就到內存池中取, 其實就是從startOfPool開始的位置劃出所須要的空間.

 

最後直接和內存池接觸的固然就是blockAlloc了, 因此咱們也來看一下這個函數.

 1 char* alloc::blockAlloc(size_t size, size_t& num) {
 2     char* re = 0;
 3     size_t bytesNeeded = size * num;
 4     size_t bytesLeft = endOfPool - startOfPool;
 5 
 6     if (bytesLeft >= bytesNeeded) {
 7         re = startOfPool;
 8         startOfPool = startOfPool + bytesNeeded;
 9         return re;
10     }
11     else if (bytesLeft > size) {
12         num = bytesLeft / size;
13         re = startOfPool;
14         startOfPool += num * size;
15         return re;
16     }
17     else {
18         //TODO
19     }
20 }

這裏原本有三種狀況, 第一種是說若是空間足夠(足夠分配20個Node那麼大), 就直接分配, 而後把指向內存池中空間起始位置的startOfPool移到新的位置, 第二種是雖然不夠分配20個, 可是足夠分配一個, 此時使用相同的方式, 只不過須要對num進行改動(由於這裏num傳的是引用, 因此也沒什麼大問題), 最後一種狀況是說連一個Node的內存都拿不出來, 這種狀況須要再向系統申請內存, 我將在下面詳細說明. 這裏咱們先來理一理, 目前的狀況...

 

1. 使用allocate向內存池請求size大小的內存空間.

2. allocate根據size找到最適合的自由鏈表.

  a. 若是鏈表不爲空, 返回第一個node, 鏈表頭改成第二個node.

  b. 若是鏈表爲空, 使用blockAlloc請求分配node.

    x. 若是內存池中有大於一個node的空間, 分配竟可能多的node(可是最多20個), 將一個node返回, 其餘的node添加到鏈表中.

    y. 若是內存池只有一個node的空間, 直接返回給用戶.

    z. 若果若是連一個node都沒有, 再次向操做系統請求分配內存(這就是上面代碼中的TODO部分).

 

而後咱們還能發現內存池的幾個特色 :

1. 剛開始初始化內存池的時候, 其實內存池中並無內存, 同時全部的自由鏈表都爲空鏈表.

2. 只有用戶第一次向內存池請求內存時, 內存池會依次執行上述過程的 1->2->b->z來完成內存池以及鏈表的首次填充, 而此時, 其餘未使用鏈表仍然是空的.

 

有了這個總體的瞭解以後, 咱們如今就來看一下, 內存池是如何向操做系統申請內存的 : 

 

 1 char* alloc::blockAlloc(size_t size, size_t& num) {
 2     char* re = 0;
 3     size_t bytesNeeded = size * num;
 4     size_t bytesLeft = endOfPool - startOfPool;
 5 
 6     if (bytesLeft >= bytesNeeded) {
 7         re = startOfPool;
 8         startOfPool = startOfPool + bytesNeeded;
 9         return re;
10     }
11     else if (bytesLeft > size) {
12         num = bytesLeft / size;
13         re = startOfPool;
14         startOfPool += num * size;
15         return re;
16     }
17     else {
18         // I am not sure why add ROUND_UP(poolSize >> 4)
19         size_t bytesToGet = 2 * bytesNeeded + ROUND_UP(poolSize >> 4);
20         if (bytesLeft > 0) {
21             node** theMostSuitableList = freeLists + FREELIST_INDEX(bytesLeft);
22             (reinterpret_cast<node*>(startOfPool))->next = *theMostSuitableList;
23             *theMostSuitableList = reinterpret_cast<node*>(startOfPool);
24         }
25 
26         startOfPool = (char*)malloc(bytesToGet);
27         if (!startOfPool) {
28             node** currentFreeList = 0;
29             node* listHeadNode = 0;
30             for (int i = size + Align; i <= MaxBytes; i += Align) {
31                 currentFreeList = freeLists + FREELIST_INDEX(i);
32                 listHeadNode = *currentFreeList;
33                 if (listHeadNode) {
34                     *currentFreeList = listHeadNode->next;
35                     startOfPool = reinterpret_cast<char*>(listHeadNode);
36                     endOfPool = reinterpret_cast<char*>(listHeadNode + i);
37                     return blockAlloc(size, num);
38                 }
39             }
40             //if code can run into this place, it means we can no longer get any memeory, so the best way is to throw exception...
41             exit(3);
42         }
43         else {
44             poolSize += bytesToGet;
45             endOfPool = startOfPool + bytesToGet;
46             return blockAlloc(size, num);
47         }
48     }
49 }

 

你會發現空間不足的時候, 首先計算了所須要的內存就是這個bytesToGet, 我在代碼中也提到了我也不太清楚後面爲何要加上一個round_up(...), 而後是把當前剩餘的內存(若是有剩餘的話)分配給合適的節點, 由於每次分配內存都是8的倍數, 因此只要有剩餘, 也確定是8把的倍數, 因此必定能找到合適的節點. 接着就開始分配內存, 若是分配內存失敗的話, 那麼從size + Align開始(其實源代碼好像是從size開始, 可是我感受此時存有size大小node的自由鏈表顯然是空的, 否則也不會調用這個函數, 因此就直接size + align 開始了), 若是能從那些位置挪出一個node的話(顯然挪出來的node要更大), 那麼就能夠完成分配了, 若是遍歷了全部比size大的節點都尋找不到這樣一塊node的話, 正如我代碼中所說的, 運行到那個位置就應該拋異常了. 另外若是分配成功, 更新相應的變量以後, 再次調用該函數進行分配, 此時內存池中有足夠的內存分配給自由鏈表.

 

早這裏關於內存的分配的全過程就講完了, 下面是內存的釋放 :

 1 void alloc::deallocate(void* ptr, size_t size) {
 2     if (size > MaxBytes) {
 3         free(ptr);
 4     }
 5     else {
 6         size_t index = FREELIST_INDEX(size);
 7         static_cast<node*>(ptr)->next = freeLists[index];
 8         freeLists[index] = static_cast<node*>(ptr);
 9     }
10 }

內存的釋放很簡單, 若是大於128bytes的, 直接釋放(由於也是直接分配過來的), 不然把它掛到相應的鏈表中, 留待以後使用.

 

到這裏, 內存池的實現就算所有講完了, 可是在真正將它投入到stl的實際使用中以前, 還要進行一層封裝.

    public:
        typedef T            value_type;
        typedef T*            pointer;
        typedef const T*    const_pointer;
        typedef T&            reference;
        typedef const T&    const_reference;
        typedef size_t        size_type;
        typedef ptrdiff_t    difference_type;
    public:
        static T *allocate();
        static T *allocate(size_t n);
        static void deallocate(T *ptr);
        static void deallocate(T *ptr, size_t n);

        static void construct(T *ptr);
        static void construct(T *ptr, const T& value);
        static void destroy(T *ptr);
        static void destroy(T *first, T *last);
    };

    template<class T>
    T *allocator<T>::allocate(){
        return static_cast<T *>(alloc::allocate(sizeof(T)));
    }
    template<class T>
    T *allocator<T>::allocate(size_t n){
        if (n == 0) return 0;
        return static_cast<T *>(alloc::allocate(sizeof(T) * n));
    }
    template<class T>
    void allocator<T>::deallocate(T *ptr){
        alloc::deallocate(static_cast<void *>(ptr), sizeof(T));
    }
    template<class T>
    void allocator<T>::deallocate(T *ptr, size_t n){
        if (n == 0) return;
        alloc::deallocate(static_cast<void *>(ptr), sizeof(T)* n);
    }

    template<class T>
    void allocator<T>::construct(T *ptr){
        new(ptr)T();
    }
    template<class T>
    void allocator<T>::construct(T *ptr, const T& value){
        new(ptr)T(value);
    }
    template<class T>
    void allocator<T>::destroy(T *ptr){
        ptr->~T();
    }
    template<class T>
    void allocator<T>::destroy(T *first, T *last){
        for (; first != last; ++first){
            first->~T();
        }
    }
}

這也就是咱們熟悉的標準庫中的allocator的接口...

 

因此最終內存池的思路實際上是這樣的:

1. 使用allocate向內存池請求size大小的內存空間, 若是須要請求的內存大小大於128bytes, 直接使用malloc.

2. 若是須要的內存大小小於128bytes, allocate根據size找到最適合的自由鏈表.

  a. 若是鏈表不爲空, 返回第一個node, 鏈表頭改成第二個node.

  b. 若是鏈表爲空, 使用blockAlloc請求分配node.

    x. 若是內存池中有大於一個node的空間, 分配竟可能多的node(可是最多20個), 將一個node返回, 其餘的node添加到鏈表中.

    y. 若是內存池只有一個node的空間, 直接返回給用戶.

    z. 若果若是連一個node都沒有, 再次向操做系統請求分配內存.

      ①分配成功, 再次進行b過程

      ②分配失敗, 循環各個自由鏈表, 尋找空間

        I. 找到空間, 再次進行過程b

        II. 找不到空間, 拋出異常(代碼中並未給出, 只是給出了註釋)

3. 用戶調用deallocate釋放內存空間, 若是要求釋放的內存空間大於128bytes, 直接調用free.

4. 不然按照其大小找到合適的自由鏈表, 並將其插入.

 

特色實際上是這樣的 :

1. 剛開始初始化內存池的時候, 其實內存池中並無內存, 同時全部的自由鏈表都爲空鏈表.

2. 只有用戶第一次向內存池請求內存時, 內存池會依次執行上述過程的 1->2->b->z來完成內存池以及鏈表的首次填充, 而此時, 其餘未使用鏈表仍然是空的.

3. 全部已經分配的內存在內存池中沒有任何記錄, 釋放與否徹底靠程序員自覺.

4. 釋放內存時, 若是大於128bytes, 則直接free, 不然加入相應的自由鏈表中而不是直接返還給操做系統.

 

以上是我對於sgi stl內存池的理解, 若是有任何不對的地方, 歡迎指出, 謝謝...

相關文章
相關標籤/搜索