一个Generic Memory Pool的剖析
By 凝霜(Loki)
一个人的战争(http://blog.youkuaiyun.com/MDL13412)
这个项目的主页在http://sourceforge.net/projects/memorypool/,个人感觉代码写的比较好,设计的也很精巧,特此剖析。
为了尽量减少代码,我会将一些注释去掉,并用我的注释替换,另外,对于原作者的版权声明,我仅在文章开头给出,后面代码中将其去掉。
/*!
\author Matt Wash
*/
首先,这个项目的源码文件夹中包含8个文件:
ExampleClasses.h main.cpp Pool.h GenericMemoryPool.sln MattExampleClasses.h Uncopyable.h GenericMemoryPool.vcproj PooledObject.h
其中Pool.h PooledObject.h Uncopyable.h是核心代码,ExampleClasses.h MattExampleClasses.h main.cpp 是测试代码,GenericMemoryPool.sln和GenericMemoryPool.vcproj是Visual Studio 2003及以上版本组织项目使用的,由于我是在Linux下剖析的,使用的IDE是NetBeans,所以这两个文件对我的剖析没有任何用处,我直接将其删除,然后使用NetBeans重新组织项目。
让我们从最小 的Uncopyable.h开始剖析
#ifndef UNCOPYABLE_H
#define UNCOPYABLE_H
// 这个是类的作用是阻止一个类被拷贝(包括复制构造和赋值操作)
//
// 其原理很简单, 就是将复制构造函数和operator =声明为private,
// 由于并不真正使用, 所以只要函数声明, 不需要实现.
// 然后任何不想被拷贝的类D从Uncopyable继承, 这样如果对类D进行
// 拷贝操作就会由于Uncopyable声明的函数访问权限不足而失败.
//
// 详细资料请参考 <Effective C++>
class Uncopyable
{
protected:
Uncopyable() {}
~Uncopyable() {}
private:
Uncopyable(const Uncopyable&);
Uncopyable& operator=(const Uncopyable&);
};
#endif // UNCOPYABLE_H
接下来我们剖析
Pool.h
#ifndef POOL_H
#define POOL_H
// Comment By: 凝霜
// E-mail: mdl2009@vip.qq.com
// Blog: http://blog.youkuaiyun.com/mdl13412
#include <new>
#include "Uncopyable.h"
// Pool中最多能存储的实例数量, 超出最大数量会导致错误,
// 在Debug版本中会对分配进行检测, 超过最大数量会触发assert
const int g_MaxNumberOfObjectsInPool = 1000;
// 首先是简述一下代码的公共接口:
// static void *Allocate () // 从Pool中分配一个实例
// static void Free (void *ptr) // 将一个从Pool中分配的实例释放(归还Pool)
//
// static void Create () // 显式创建Pool
// static void Destroy () // 显式销毁Pool
//
// ~Pool() // 销毁Pool
// 程序框架设计的很小巧, 思路也很清晰, 我主要分析内存对齐分配的问题
// 另外, 这个Pool用了Singleton模式
template <class T>
class Pool : private Uncopyable
{
public:
static void* Allocate()
{
// 检测Pool是否已经分配, 防止发生错误
if (instance().memory_ == NULL) instance().createPool();
// Debug模式下检测Pool是否还有容量
assert(instance().numFreeBlocks_ > 0 && "the pool is empty");
// 分配实例对象
return instance().allocateBlock();
}
static void Free(void* ptr)
{
// 检测Pool是否分配以及待释放指针是否为空, 以防止发生错误
if (!instance().memory_ || !ptr) return;
// 在Debug模式下检测待释放对方是否是在Pool分配的
assert(instance().containsPointer(ptr)
&& "object not allocated from this pool");
// 将释放的内存还给Pool
instance().freeBlock((FreeBlock*) ptr);
}
static void Create()
{
if (instance().memory_ == NULL) instance().createPool();
}
static void Destroy()
{
if (instance().memory_ != NULL) instance().destroyPool();
}
~Pool() { destroyPool(); }
private:
// 用来维护内存链表, 这个struct是为了提高可读性
struct FreeBlock { FreeBlock* next; };
Pool() : memory_(NULL), head_(NULL), blockSize_(0) { }
static Pool& instance()
{
static Pool pool;
return pool;
}
// 这段代码是本框架的难点, 但同时也是亮点
void createPool()
{
numFreeBlocks_ = g_MaxNumberOfObjectsInPool; // Pool容量
// 内存块的大小要满足内存对齐的要求, 这样才能使CPU寻址最快.
// __alignof(T)是为了检测T对齐的粒度, 因为用户可以指定对齐的粒度,
// 所以不可以Hard Code, 在早期的STL内存配置器中, 对齐粒度固定为8.
// 但是如果用户指定对齐为16, 那么就会出现错误, 这里的动态检测是亮点
//
// 举个例子, 假设sizeof(T) = 20, __alignof(T) = 16
// 那么 blockSize = 20
// diff = 20 % 16 = 4
// 因为 diff != 0, 所以
// blockSize += 20 + 16 - 4 = 32
// 这样就满足内存对齐的要求了, 很不错的算法
//
// 这里有一个问题, 如果sizeof(T)比一个指针要小, 那么会浪费内存
blockSize_ = sizeof (T);
size_t diff = blockSize_ % __alignof(T);
if (diff != 0) blockSize_ += __alignof(T) - diff;
// 注意: 如果分配的blockSize比一个指针还小, 那么就至少要分配一个指针的大小
if (blockSize_ < sizeof (uintptr_t)) blockSize_ = sizeof (uintptr_t);
memory_ = alignedMalloc(g_MaxNumberOfObjectsInPool * blockSize_, __alignof(T));
// 检测分配内存是否满足内存对齐条件, 不过个人感觉没必要进行检测
assert(isAligned(memory_, __alignof(T)) && "memory not aligned");
// 将FreeBlock链表头设置为分配的值
head_ = (FreeBlock*) memory_;
head_->next = NULL;
}
void destroyPool()
{
alignedFree(memory_);
memory_ = NULL;
}
// 检测一个指针是否是在Pool中分配的, 用于防止错误释放
bool containsPointer(void* ptr)
{
return (uintptr_t) ptr >= (uintptr_t) memory_ &&
(uintptr_t) ptr < (uintptr_t) memory_ + blockSize_
* g_MaxNumberOfObjectsInPool;
}
FreeBlock* allocateBlock()
{
// 分配block是一个O(1)的算法,链表头始终是空闲节点
FreeBlock* block = head_;
// 这里维护的是空闲节点的数目, 即Pool的剩余容量
if (--numFreeBlocks_ != 0)
{
if (head_->next == NULL)
{
// If the block has not been previously allocated its next pointer
// will be NULL so just update the list head to the next block in the pool
head_ = (FreeBlock*) (((uintptr_t) head_) + blockSize_);
head_->next = NULL;
}
else
{
// The block has been previously allocated and freed so it
// has a valid link to the next free block
head_ = head_->next;
}
}
return block;
}
void freeBlock(FreeBlock* block)
{
// 将内存归还到链表头
if (numFreeBlocks_ > 0) block->next = head_;
head_ = block;
// 维护空闲节点数目
numFreeBlocks_++;
}
void* alignedMalloc(size_t size, int alignment)
{
// 分配足够的内存, 这里的算法很经典, 早期的STL中使用的就是这个算法
// 首先是维护FreeBlock指针占用的内存大小
const int pointerSize = sizeof (void*);
// alignment - 1 + pointerSize这个是FreeBlock内存对齐需要的内存大小
// 前面的例子sizeof(T) = 20, __alignof(T) = 16,
// g_MaxNumberOfObjectsInPool = 1000
// 那么调用本函数就是alignedMalloc(1000 * 20, 16)
// 那么alignment - 1 + pointSize = 19
const int requestedSize = size + alignment - 1 + pointerSize;
// 分配的实际大小就是20000 + 19 = 20019
void* raw = malloc(requestedSize);
// 这里实Pool真正为对象实例分配的内存地址
uintptr_t start = (uintptr_t) raw + pointerSize;
// 这个算法的剖析见我剖析的STL源码的<stl_alloc.h>第355行
// http://blog.youkuaiyun.com/mdl13412/article/details/6638405
void* aligned = (void*) ((start + alignment - 1) & ~(alignment - 1));
// 这里维护一个指向malloc()真正分配的内存
*(void**) ((uintptr_t) aligned - pointerSize) = raw;
// 返回实例对象真正的地址
return aligned;
}
// 这里是内部维护的内存情况
// 这里满足内存对齐要求
// |
// ----------------------------------------------------------------------
// | 内存对齐填充 | 维护的指针 | 对象1 | 对象2 | 对象3 | ...... | 对象n |
// ----------------------------------------------------------------------
// ^ | 指向malloc()分配的地址起点
// | |
// -----------------------
void alignedFree(void* aligned)
{
// 释放操作很简单了, 参见上图
void* raw = *(void**) ((uintptr_t) aligned - sizeof (void*));
free(raw);
}
bool isAligned(void* data, int alignment)
{
// 又是一个经典算法, 参见<Hacker's Delight>
return ((uintptr_t) data & (alignment - 1)) == 0;
}
private:
void* memory_; // A pointer to the memory allocated for the entire pool
FreeBlock* head_; // A pointer to the head of the linked list of free blocks
size_t numFreeBlocks_; // The current number of free blocks in the pool
size_t blockSize_; // The size in bytes of a block in the pool (this is only
// stored so it can be used by containsPointer() to
// validate deallocation and so could be omitted in a release build)
};
// 显式创建Pool
#define CREATE_POOL(T) \
Pool<T>::Create();
// 显式销毁Pool
#define DESTROY_POOL(T) \
Pool<T>::Destroy();
// 分配用户自定义类型, __VA_ARGS__是可变宏参数, 用于class的初始化
#define POOL_NEW(T, ...) \
new(Pool<T>::Allocate()) T(__VA_ARGS__) \
// 销毁用户自定义类型
#define POOL_DELETE(T, ptr) \
ptr->~T(); \
Pool<T>::Free(ptr);
// 用于POD类型, 一切为了效率
#define POOL_ALLOC(T) \
(T*)Pool<T>::Allocate();
#define POOL_FREE(T, ptr) \
Pool<T>::Free(ptr);
#endif // POOL_H
总结:
可以改进的地方:
1) Pool的容量应该由用户自定义,不应该使用全局变量,这样自由度很低
2) 对于Pool操作宏,可以使用Traits技术根据POD和用户自定义类型进行编译期派发,而不用维护两套不同的宏,这样可以实现对程序员透明
3) 内存对齐检查这个过程个人认为只有在写本框架的时候才有用,开发完成后应该去掉
4) Uncopyable这个类其实可以去掉, 因为其它地方都没有用到,用户也不会去复用,那么直接将Pool复制构造函数和operator =声明为private即可。
5) 这个框架不是线程安全的,至少应该在创建Pool的整个过程中lock,否则会出现问题。
6) Pool没有容量的时候不能正确工作,应该增加一个二级分配器,应对这种情况,效率可以适当放要求
7) 对于sizeof小于指针的对象,会浪费内存,应该进行特化,提供高性能的版本
做的不错的地方:
1) 对FreeBlock的定义,提高了程序可读性
2) 内存对齐分配问题,速度快,而且能减少内存碎片的产生
3) 内存对齐算法使用了__alignof进行检测,没有Hard Code,避免了Hard Code引发的问题
4) 接口设计的很好,Create()接口给程序员自己控制Pool建立的时机,非常好
5) 获取对象实例的时间复杂度为O(1),很好
6) Singleton模式运用的很好
7) 文档写的很好,讲解的也不错:-)