一个Generic Memory Pool的剖析

一个Generic Memory Pool的剖析

By 凝霜(Loki)

一个人的战争(http://blog.csdn.net/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.slnGenericMemoryPool.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.csdn.net/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.csdn.net/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) 文档写的很好,讲解的也不错:-)

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值