最近这两天研究了一下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内存池的理解, 若是有任何不对的地方, 欢迎指出, 谢谢...