剖析STL空间配置器

来源:互联网 发布:疯狂网络txt下载 编辑:程序博客网 时间:2024/05/22 17:22

本文将围绕以下几方面逐步对STL空间配置器进行一个剖析
1.为什么要有空间配置器,它是用来解决什么问题的?
2.STL中标准版本带来什么问题?如何解决的?
3.探索STL中的SGI版空间配置器。
  (1)一级空间配置器该如何做的?
  (2)二级空间配置器又是怎样的一个工作流程?
4.STL中特殊空间配置器(SGI版)的代码实现
5.实现一个固定大小的内存池



1.为什么要有空间配置器,它是用来解决什么问题的?
相信熟悉vector的同学可能知道,每次push操作后它的capacity是按照1,2,3,4,6,6,9,9……来增长的,那么它每次是如何扩容的呢?为什么敢这么肆无忌惮的一点一点去开辟空间出来使用?这就源于空间配置器的功劳。
如果一到开辟空间就使用malloc,那么可能会引发两方面的问题:
(1)小块内存分配带来的内存碎片(外碎片)问题
   由于频繁分配、释放小块内存容易在堆中造成外碎片(也就是说堆中空闲的内存总量明明满足一个分配请求,但是这些空闲的块都不连续,导致任何一个单独的空闲的块都无法满足这个请求)。

(2)小块内存频繁申请,释放带来的性能问题
  开辟空间的时候,分配器会去找一块空闲块给用户,找空闲块也是需要时间的,尤其是在外碎片比较多的情况下。如果分配器其找不到,就要考虑处理假碎片现象(释放的小块空间没有合并),这时候就要将这些已经释放的的空闲块进行合并,这也是需要时间的。
  malloc在开辟空间的时候,这些空间会带有一些附加的信息(比如分配的长度,类型等信息,利于回收,所以不是说你申请一个字节就从堆里只取了一个字节),这样的话也就造成了空间的利用率有所降低,尤其是在频繁申请小块内存的时候。


2.早期STL空间配置器的标准版本设计
为了解决以上两个主要的问题就引来了内存池的概念,早期STL标准版本是这样设计的,一次向heap申请一块很大的内存(内存池),如果申请小块内存的话就直接到内存池中去要。这样的话,就能够有效的解决上面所提到的问题。下图就是这样一个模型:


这样虽然在某种程度上避免了频繁从堆中去分配小块内存所造成的影响,但是又引发了一个问题,如果分配出去的内存用完了,该如何还回来,如何再很好的去维护它呢?针对这一系列问题,STL空间配置器的SGI版提出了自由链表的思想,下面就来看一下它的内部是如何实现的。

3.STL空间配置器的SGI版内部设计
STL里面的空间配置主要分为两级,一级空间配置器(__malloc_alloc_template)和二级空间配置器(__default_alloc_template)。在STL空间配置器里默认分配128字节以上的就为大块内存的分配,调用一级空间配置器直接从系统堆中获得;128字节及以下的认为是小块内存的分配,调用二级空间配置器。
3.1下面是一级空间配置器的内部实现

实现流程:


3.2 二级空间配置器的实现机制
 二级空间配置器是内存池+自由链表来设计的,它内部是这样来实现的,先对要分配的内存块的大小进行一个判断,如果内存块小于或等于128字节的才调用二级空间配置器。自由链表是一个指针数组+链表的形式构成,假如要分配8个字节的空间大小,那么就会先去Memmory Pool分配多份(一般是20份)8个字节大小的内存块,拿给用户去用一块,再将剩余的挂在对应下标处,下一次若还要8个字节就直接去对应下标下面的链表上去摘取一块就好,用完了还是将它挂在自由链表上即可,这样就很好的解决了还回空间的处理,也让资源达到很好的利用。如图所示:

(1)为了便于管理,二级空间配置器在分配的时候都是对齐到8的倍数的(例如:你需要6个字节,你会得到8个字节;你需要12个字节,它会分配给你16个字节),尽管这样做可能会造成内碎片问题,但却方便了管理,这样就只需要维护16个free-lists,各自管理大小分别为8,16,24,32,40,48,56,64,72,80,88,86,96,104,112,120,128的小额区块,free-lists的节点结构如下:
 


这里有个问题需要注意一下:
为什么自由链表管理的内存大小要从8字节开始?
这主要是为了平台的可移植性而设计的,从上面也可看出free-lists的节点结构里面包含一个Obj*的指针,如果在64位平台下,一个指针的大小是8字节,所以至少要满足一个指针的大小,因此自由链表管理的内存大小要从8字节开始。

(2)空间配置器的重要函数的具体实现过程:
     1> refill函数:是在对应大小的自由链表的下边没有可以使用的空闲缓冲块的时候,需要调用此函数来填充自由链表。
2> chunk_alloc函数:就是去内存池中申请空间使用,如果内存池也没有可以使用的,则调用malloc去开辟。下面是剖析的STL中实现的该函数。

3>二级空间配置器在分配的时候都是对齐到8的倍数,以及求数组下标的函数,源码中在这些方面的设计还是很有亮点的.



(3)STL空间配置器的实现流程图:

这里没有详细说chunk_alloc这个函数,但是函数实现中已经详细加了注释:
template<bool threads, int inst>char*  DefaultAllocTemplate<threads, inst>::ChunkAlloc(size_t size, int& nobjs){char* result;size_t bytesNeed = size*nobjs;size_t bytesLeft = _endFree - _startFree;// 1.内存池中的内存足够,bytesLeft>=bytesNeed,则直接从内存池中取。// 2.内存池中的内存不足,但是够一个bytesLeft >= size,则直接取能够取出来。// 3.内存池中的内存不足,则从系统堆分配大块内存到内存池中。if (bytesLeft >= bytesNeed){__TRACE_DEBUG("内存池中内存足够分配%d个对象\n", nobjs);result = _startFree; _startFree += bytesNeed;}else if(bytesLeft >= size){__TRACE_DEBUG("内存池中内存不够分配%d个对象,只能分配%d个对象\n", nobjs, bytesLeft / size);result = _startFree;nobjs = bytesLeft / size;_startFree += nobjs*size;}else{//若内存池中还有小块剩余内存,则将它头插到合适的自由链表if (bytesLeft> 0){size_t index = FREELIST_INDEX(bytesLeft);((Obj*)_startFree)->_freeListLink = _freeList[index];_freeList[index] = (Obj*)_startFree;_startFree = NULL;__TRACE_DEBUG("将内存池中剩余的空间,分配给freeList[%d]\n", index);}//从系统堆分配两倍+已分配的heapSize/8的内存到内存池中size_t bytesToGet = 2 * bytesNeed + ROUND_UP(_heapSize >> 4);_startFree = (char*)malloc(bytesToGet);__TRACE_DEBUG("内存池空间不足,系统堆分配%u bytes内存\n", bytesToGet);//如果在系统堆中内存分配失败,则尝试到自由链表中更大的节点中分配if (_startFree == NULL){__TRACE_DEBUG("系统堆已不够,无奈之下,只能到自由链表中去找\n");for (int i = size; i <= __MAX_BYTES; i += __ALIGN){Obj* head = _freeList[FREELIST_INDEX(size)];if (head){_startFree = (char*)head;_freeList[FREELIST_INDEX(size)] = head->_freeListLink;_endFree = _startFree + i;return ChunkAlloc(size, nobjs);}}//自由链表中也没有分配到内存,则再到一级配置器中分配内存,//一级配置器中可能有设置的处理内存,或许能分配到内存。__TRACE_DEBUG("系统堆和自由链表都已无内存,一级配置器做最后一根稻草\n");_startFree = (char*)MallocAllocTemplate<0>::Allocate(bytesToGet);}//从系统堆分配的总字节数。(可用于下次分配时进行调节)_heapSize += bytesToGet;_endFree = _startFree + bytesToGet;//递归调用获取内存return ChunkAlloc(size, nobjs);}return result;}

当然还有释放的情况,如果是大块内存的释放则直接调一级空间配置器中的deallocate,底层是free实现的,如果是小块内存的释放则将释放回来的内存继续挂在自由链表的对应位置上。

4.在源码的基础上照着它的思路对其进行了代码的实现,这份代码中加了一个跟踪日志,相当于对空间配置器进行了一个白盒测试。

#define _CRT_SECURE_NO_WARNINGS 1#include<iostream>#include<windows.h>#include<vector>using namespace std;// Trace 跟踪 #define __DEBUG__  FILE* fout = fopen("message.log", "w");   //打开一个message.log文件,可以将日志信息写进该文件中static string GetFileName(const string& path){char ch = '/';#ifdef _WIN32  ch = '\\';#endif  size_t pos = path.rfind(ch);if (pos == string::npos)return path;elsereturn path.substr(pos + 1);}//用于调试追溯的trace log  inline static void __trace_debug(const char* function, const char * filename, int line, char* format, ...){// 读取配置文件  #ifdef __DEBUG__  // 输出调用函数的信息  fprintf(stdout, "【%s:%d】%s", GetFileName(filename).c_str(), line, function);fprintf(fout, "【%s:%d】%s", GetFileName(filename).c_str(), line, function);// 输出用户打的trace信息  va_list args;va_start(args, format);vfprintf(stdout, format, args);vfprintf(fout, format, args);fprintf(fout, "%c", '\n');fprintf(stdout, "%c", '\n');va_end(args);#endif  }#define __TRACE_DEBUG(...)  __trace_debug(__FUNCTION__, __FILE__, __LINE__, __VA_ARGS__);//STL中大于128字节的都认为是大块分配,调用一级空间配置器//一级空间配置器(malloc/realloc/free)//内存分配失败以后处理的句柄handler类型typedef void(*ALLOC_OOM_FUN)();      //函数指针ALLOC_OOM_FUNtemplate<int inst>class MallocAllocTemplate{private:static ALLOC_OOM_FUN __MallocAllocOomHandler;static void* OomMalloc(size_t n){ALLOC_OOM_FUN handler;void* result;        // (1)分配内存成功,则直接返回;        // (2)若分配失败,则检查是否设置处理的handler,//有则调用以后再分配。不断重复这个过程,直到分配成功为止。//没有设置处理的handler,则直接结束程序。//这里不用while(1)来写死循环,是因为这样效率较高for (;;) {handler = __MallocAllocOomHandler;if (0 == handler){cerr << "out of memory" << endl;  //类似于Linux下的perrorexit(-1); }handler();result = malloc(n);if (result)return result;   //直到分配出来了空间才跳出该循环,返回一块申请到的空间}}static void* OomRealloc(void* p, size_t n){//同上ALLOC_OOM_FUN handler;void* result;for (;;) {handler = __MallocAllocOomHandler;if (0 == handler){cerr << "out of memory" << endl;exit(-1);}(*handler)();result = realloc(p, n);if (result)return(result);}}public:static void* Allocate(size_t n){__TRACE_DEBUG("一级空间配置器分配空间%u个\n", n);void* result = malloc(n);if (0 == result) result = OomMalloc(n);return result;}static void Deallocate(void* ptr){__TRACE_DEBUG("一级空间配置器释放空间0x%p", ptr);free(ptr);ptr = NULL;}static void* Reallocate(void* ptr,size_t new_sz){void* result = realloc(ptr, new_sz);if (0 == result)result = OomRealloc(ptr, new_sz);return result;}static ALLOC_OOM_FUN SetMallocHandler(ALLOC_OOM_FUN f){__TRACE_DEBUG("一级空间配置器分配失败设置Handler处理机制");ALLOC_OOM_FUN old = __MallocAllocOomHandler;__MallocAllocOomHandler = f;return old;}};//分配内存失败处理函数的句柄函数指针template<int inst>ALLOC_OOM_FUN MallocAllocTemplate<inst>::__MallocAllocOomHandler = 0;//二级空间配置器enum{ __ALIGN = 8 }; //排列基准值(也是排列间隔)enum{ __MAX_BYTES = 128 }; //最大值enum{ __NFREELISTS = __MAX_BYTES / __ALIGN }; //free-lists个数template<bool threads, int inst>class DefaultAllocTemplate{private:static size_t ROUND_UP(size_t bytes)   {//对齐到8的倍数return((bytes + __ALIGN - 1) & ~(__ALIGN - 1));}static size_t FREELIST_INDEX(size_t bytes){//(bytes + __ALIGN - 1)跃迁到两个跨度之间,再除去__ALIGN就算出第几个区块//然后减去1就得出指针数组的下标return((bytes + __ALIGN - 1) / __ALIGN - 1);}union Obj{ union Obj* _freeListLink; //指向下一个内存块的指针    char  _clientData[1];    //The client sees this.};static Obj* volatile _freeList[__NFREELISTS]; //自由链表static char* _startFree; //内存池水位线开始static char* _endFree; //内存池水位线结束static size_t _heapSize; //从系统堆分配的总大小//获取大块内存插入到自由链表中static void* Refill(size_t n);//从内存池中分配大块内存static char* ChunkAlloc(size_t size, int& nobjs);public:    //只暴露这三部分给用户static void* Allocate(size_t n);static void Deallocate(void* ptr, size_t n);static void* Reallocate(void* ptr, size_t old_sz, size_t new_sz);};//初始化全局静态对象template<bool threads, int inst>//前面加typename表明它后面的是一个类型,自由链表是16个typename DefaultAllocTemplate<threads, inst>::Obj* volatile DefaultAllocTemplate<threads, inst>::\_freeList[__NFREELISTS] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };template<bool threads, int inst>char* DefaultAllocTemplate<threads,inst>::_startFree = 0;template<bool threads, int inst>char* DefaultAllocTemplate<threads,inst>::_endFree = 0;template<bool threads, int inst>size_t DefaultAllocTemplate<threads,inst>::_heapSize = 0;template<bool threads, int inst>void* DefaultAllocTemplate<threads, inst>::Refill(size_t n){int nobjs = 20;__TRACE_DEBUG("自由链表中没有内存则通过Refill进行填充%u个\n", FREELIST_INDEX(n), nobjs);//分配n bytes的内存//如果不够则能分配多少分配多少char* chunk = ChunkAlloc(n, nobjs);//如果只分配到一块,则直接返回这块内存if (nobjs == 1)return chunk;Obj* result, *cur;size_t index = FREELIST_INDEX(n);result = (Obj*)chunk;//把剩余的块链接到自由链表上面cur = (Obj*)(chunk + n);_freeList[index] = cur;for (int i = 2; i<nobjs; ++i){cur->_freeListLink = (Obj*)(chunk + n*i);cur = cur->_freeListLink;}cur->_freeListLink = NULL;return result;}template<bool threads, int inst>char*  DefaultAllocTemplate<threads, inst>::ChunkAlloc(size_t size, int& nobjs){char* result;size_t bytesNeed = size*nobjs;size_t bytesLeft = _endFree - _startFree;// 1.内存池中的内存足够,bytesLeft>=bytesNeed,则直接从内存池中取。// 2.内存池中的内存不足,但是够一个bytesLeft >= size,则直接取能够取出来。// 3.内存池中的内存不足,则从系统堆分配大块内存到内存池中。if (bytesLeft >= bytesNeed){__TRACE_DEBUG("内存池中内存足够分配%d个对象\n", nobjs);result = _startFree; _startFree += bytesNeed;}else if(bytesLeft >= size){__TRACE_DEBUG("内存池中内存不够分配%d个对象,只能分配%d个对象\n", nobjs, bytesLeft / size);result = _startFree;nobjs = bytesLeft / size;_startFree += nobjs*size;}else{//若内存池中还有小块剩余内存,则将它头插到合适的自由链表if (bytesLeft> 0){size_t index = FREELIST_INDEX(bytesLeft);((Obj*)_startFree)->_freeListLink = _freeList[index];_freeList[index] = (Obj*)_startFree;_startFree = NULL;__TRACE_DEBUG("将内存池中剩余的空间,分配给freeList[%d]\n", index);}//从系统堆分配两倍+已分配的heapSize/8的内存到内存池中size_t bytesToGet = 2 * bytesNeed + ROUND_UP(_heapSize >> 4);_startFree = (char*)malloc(bytesToGet);__TRACE_DEBUG("内存池空间不足,系统堆分配%u bytes内存\n", bytesToGet);//如果在系统堆中内存分配失败,则尝试到自由链表中更大的节点中分配if (_startFree == NULL){__TRACE_DEBUG("系统堆已不够,无奈之下,只能到自由链表中去找\n");for (int i = size; i <= __MAX_BYTES; i += __ALIGN){Obj* head = _freeList[FREELIST_INDEX(size)];if (head){_startFree = (char*)head;_freeList[FREELIST_INDEX(size)] = head->_freeListLink;_endFree = _startFree + i;return ChunkAlloc(size, nobjs);}}//自由链表中也没有分配到内存,则再到一级配置器中分配内存,//一级配置器中可能有设置的处理内存,或许能分配到内存。__TRACE_DEBUG("系统堆和自由链表都已无内存,一级配置器做最后一根稻草\n");_startFree = (char*)MallocAllocTemplate<0>::Allocate(bytesToGet);}//从系统堆分配的总字节数。(可用于下次分配时进行调节)_heapSize += bytesToGet;_endFree = _startFree + bytesToGet;//递归调用获取内存return ChunkAlloc(size, nobjs);}return result;}template<bool threads, int inst>void* DefaultAllocTemplate<threads, inst>::Allocate(size_t n){////否则在二级配置器中获取if (n>__MAX_BYTES){__TRACE_DEBUG("若n > __MAX_BYTES则直接在一级配置器中获取%d个\n", n);return MallocAllocTemplate<0>::Allocate(n);}size_t index = FREELIST_INDEX(n);void* ret = NULL;// 1.如果自由链表中没有内存则通过Refill进行填充    // 2.如果自由链表中有则直接返回一个节点块内存    // ps:多线程环境需要考虑加锁Obj* head = _freeList[index];if (head == NULL){return Refill(ROUND_UP(n));}else{__TRACE_DEBUG("自由链表取内存:_freeList[%d]\n", index);_freeList[index] = head->_freeListLink;return head;}}template<bool threads, int inst>void DefaultAllocTemplate<threads, inst>::Deallocate(void* ptr, size_t n){if (n>__MAX_BYTES){__TRACE_DEBUG("n > __MAX_BYTES则直接归还给一级配置器");MallocAllocTemplate<0>::Deallocate(ptr);}else{   //否则再放回二级空间配置器的自由链表size_t index = FREELIST_INDEX(n);__TRACE_DEBUG("回收内存链到自由链表中的%u的位置处", index);//头插回自由链表Obj* tmp = (Obj*)ptr;tmp->_freeListLink = _freeList[index];_freeList[index] = tmp;}}template<bool threads, int inst>void* DefaultAllocTemplate<threads, inst>::Reallocate(void* p, size_t old_sz, size_t new_sz){void* result;size_t copy_sz;if (old_sz> (size_t)__MAX_BYTES && new_sz> (size_t)__MAX_BYTES) {return(realloc(p, new_sz));}if (ROUND_UP(old_sz) == ROUND_UP(new_sz))return p;result = Allocate(new_sz);copy_sz = new_sz>old_sz ? old_sz : new_sz;memcpy(result, p, copy_sz);Deallocate(p, old_sz);return result;}//通过__TRACE_DEBUG做白盒测试void Test1(){void* p1 = MallocAllocTemplate<0>::Allocate(129);MallocAllocTemplate<0>::Deallocate(p1);void* p2 = MallocAllocTemplate<0>::Allocate(13);MallocAllocTemplate<0>::Deallocate(p2);}//测试分配释放的场景void Test2(){int begin = GetTickCount();vector<pair<void*, int> > v;v.push_back(make_pair(DefaultAllocTemplate<false, 0>::Allocate(130), 130));for (int i = 0; i < 100; ++i){v.push_back(make_pair(DefaultAllocTemplate<false, 0>::Allocate(28), 28));}while (!v.empty()){DefaultAllocTemplate<false, 0>::Deallocate(v.back().first, v.back().second);v.pop_back();}for (int i = 0; i < 100; ++i){v.push_back(make_pair(DefaultAllocTemplate<false, 0>::Allocate(28), 28));}while (!v.empty()){DefaultAllocTemplate<false, 0>::Deallocate(v.back().first, v.back().second);v.pop_back();}int end = GetTickCount();cout << end - begin << endl;}//测试系统堆内存耗尽的场景void Test3(){cout << "测试系统堆内存耗尽" << endl;char* ptr1=(char*)DefaultAllocTemplate<false, 0>::Allocate(1024 * 1024 * 1024);char* ptr2=(char*)DefaultAllocTemplate<false, 0>::Allocate(1024 * 1024);for (int i = 0; i< 100000; ++i){char* p1 = (char*)DefaultAllocTemplate<false, 0>::Allocate(128);}}int main(){//Test1();Test2();//Test3();system("pause");return 0;}//空间配置器里成员函数和成员变量都使用静态的,这样一个好处是不用对象就能调用

5.实现一个固定大小的内存池
我们知道内存池的优点就是可以减少内存碎片,分配内存更快,可以避免内存泄露等优点。实现一个固定大小的内存分配器,每次分配一个node,一个node相当于一个小的对象池,这个小池子用完了,再分配一个尺寸大一倍的node,这些node是以链表的方式链接起来的。(每一个节点管理一块内存,设定各个内存块存储对象的多少按2倍增长),下面就是具体实现这个内存池的博客链接:
实现一个固定大小的内存池


原创粉丝点击