山寨STL实现之内存池
内存池的作用:
减少内存碎片,提高性能。
首先不得不提的是Win32和x64中对于指针的长度是不同的,在Win32中一个指针占4字节,而在x64中一个指针占8字节。也正是不清楚这一点,当我在x64中将指针作为4字节修改造成其他数据异常。
首先我们先来定义三个宏
#define ALIGN sizeof(void*) #define MAX_BYTES 128 #define MAX_COUNT (MAX_BYTES / ALIGN)
正如前面所说的,为了兼容Win32与x64应此我们将要申请的内存按void*的大小来对齐。正如前面所说的,我们认为小于128字节的内存为小内存,会产生内存碎片,应此在申请时应该劲量申请一块较大的内存而将其中的一小块分配给他。
然后让我们来看一下内存池中的成员变量
struct obj { obj* next; }; struct block { block* next; void* data; }; obj* chunk_list[MAX_COUNT]; size_t chunk_count; block* free_list;
这里使用obj结构来存储已释放内存的列表,这样做的好处是可以更节省内存。在Win32中使用这块内存的前4字节来指向下一个节点,而在x64中使用这块内存的前8字节来指向下一个节点。
chunk_list:保存通过deallocate或refill中释放或是新申请的内存块列表,deallocate和refill将会在下文中介绍。
chunk_count:内存块列表中已有的内存块数量。
free_list:保存了通过malloc申请内存块的链表。
下面我们来看一下内存池的构造函数与析构函数
MemoryPool() : free_list(0), chunk_count(0) { for(int i = 0; i < MAX_COUNT; ++i) chunk_list[i] = 0; } ~MemoryPool() { block* current = free_list; while(current) { block* next = current->next; free(current->data); free(current); current = next; } }
构造函数中初始化free_list和chunk_count为0,并初始化chunk_list为一个空列表。而在析构函数中我们必须释放每一块通过malloc申请的大内存块。
接下来是内存的申请
template <typename T> T* allocate(size_t n, void(*h)(size_t)) { if(n == 0) return 0; if(n > MAX_BYTES) { T* p = (T*)malloc(n); while(p == 0) { h(n); p = (T*)malloc(n); } return p; } const int i = INDEX(ROUND_UP(n)); obj* p = chunk_list[i]; if(p == 0) { return refill<T>(i, h); } chunk_list[i] = p->next; return reinterpret_cast<T*>(p); }
值得注意的是,在调用时必须传入一个函数指针作为参数,当malloc申请内存失败时会去调用这个函数来释放出足够多的内存空间。当要申请的内存大小超过128字节时,调用默认的malloc为其申请内存。否则先查找列表中是否还有足够的空间分配给它,若已没有足够的空间分配给它,则调用refill申请一块大内存。
然后是内存释放函数deallocate
template <typename T> void deallocate(T* p, size_t n) { if(p == 0) return; if(n > MAX_BYTES) { free(p); return; } const int i = INDEX(ROUND_UP(n)); reinterpret_cast<obj*>(p)->next = chunk_list[i]; chunk_list[i] = reinterpret_cast<obj*>(p); }
值得注意的是在释放时必须给出这块内存块的大小。若这块内存大于128字节时,调用默认的free函数释放掉这块内存。否则将其加到对应的chunk_list列表内。
然后是调整内存块大小函数reallocate
template <typename T> T* reallocate(T* p, size_t old_size, size_t new_size, void(*h)(size_t)) { if(old_size > MAX_BYTES && new_size > MAX_BYTES) { return realloc(p, new_size); } if(ROUND_UP(old_size) == ROUND_UP(new_size)) return p; T* result = allocate<T>(new_size, h); const size_t copy_size = new_size > old_size ? old_size : new_size; memcpy(result, p, copy_size); deallocate<T>(p, old_size); return result; }
参数中必须给出这块内存的原始大小和要调整后的大小,同时也必须给出当内存不足时的释放函数的指针。若旧内存块和新内存块的大小都大于128字节时,调用默认的realloc函数重新分配内存。否则先按调整后的大小申请一块内存,并把原来的内容拷贝过来,最后释放掉原来的内存块。这里并不建议使用这个函数,而是手动的去重新申请内存并拷贝释放。
然后来看4个非常简单的计算函数
inline size_t ROUND_UP(size_t bytes)const { return (bytes + ALIGN - 1) & ~(ALIGN - 1); } inline size_t ROUND_DOWN(size_t bytes)const { return bytes & ~(ALIGN - 1); } inline int INDEX(size_t bytes)const { return (bytes + ALIGN - 1) / ALIGN - 1; } inline size_t obj_count(int i)const { size_t result = 0; obj* current = chunk_list[i]; while(current) { ++result; current = current->next; } return result; }
前3个用于内存对齐和计算索引,最后一个用于获取一在空闲列表内一个内存块的数量。
然后是refill函数,用于在没有空闲内存块时申请一块大内存块
template <typename T> T* refill(int i, void(*h)(size_t)) { const int count = 20; const int preSize = (i + 1) * ALIGN; char* p = (char*)malloc(preSize * count); while(p == 0) { h(preSize * count); p = (char*)malloc(preSize * count); } block* pBlock = (block*)malloc(sizeof(block)); while(pBlock == 0) { h(sizeof(block)); pBlock = (block*)malloc(sizeof(block)); } pBlock->data = p; pBlock->next = free_list; free_list = pBlock; obj* current = (obj*)(p + preSize); for(int j = 0; j < count - 1; ++j) { current->next = chunk_list[i]; chunk_list[i] = current; current = (obj*)((char*)current + preSize); } chunk_count += count - 1; rebalance(); return reinterpret_cast<T*>(p); }
首先申请一个大内存块,然后将这块申请到的内存块放入free_list链表内,最后组织起chunk_list中对应内存卡块的链表,然后重新调整chunk_list列表,最后将申请到的内存块返回。
最后来看一下调整函数rebalance
void rebalance() { for(int i = MAX_COUNT - 1; i > 0; --i) { const size_t avge = chunk_count / MAX_COUNT; size_t count = obj_count(i); if(count > avge) { const int preSize = ROUND_DOWN((i + 1) * ALIGN / 2); const int j = INDEX(preSize); for(int k = count; k > avge; --k) { obj* chunk = chunk_list[i]; chunk_list[i] = chunk_list[i]->next; if(i % 2 == 1) { chunk->next = (obj*)((char*)chunk + preSize); chunk->next->next = chunk_list[j]; chunk_list[j] = chunk; } else { chunk->next = chunk_list[j]; chunk_list[j] = chunk; obj* next = (obj*)((char*)chunk + preSize); next->next = chunk_list[j + 1]; chunk_list[j + 1] = next; } ++chunk_count; } } } }
这里从后至前查看对应内存块空闲链表的长度,若超过平均数量,则将其切分为2块较小的内存块放入对应的链表内。这样做的好处是可以形成一个金字塔形的分布状况,既越小的内存块大小拥有的节点数量越多,正如本文开头所说,使用内存池是为了解决在申请小块内存时造成的内存碎片。
至此,内存池的讲解已完成,完整的代码请到http://qlanguage.codeplex.com下载