在上个博客:游戏内存管理一之StackAllocator(堆栈分配器)中介绍了堆内存分配的弊端和堆栈分配器(StackAllocator)的实现,这篇介绍PoolAllocator(池分配器)的实现。
PoolAllocator(池分配器)
在游戏引擎编程中, 常会分配大量同等尺寸的小块内存。比如我们需要分配N个矩阵对象,N个角色对象,N个武器对象,N个可绘制Mesh对象等等。这些就是池分配器。
池分配器工作方式:预先分配一大片内存,内存刚好是某种对象的N倍。比如一个Person对象是20个字节,那么我们Pool<Person>的“角色池”分配的内存就是20字节的倍数.池分配器通常采用一个存放自由元素的链表,预先分配存在N个节点的链表。当分配对象时,从中取出一个(头指针指向下一个节点);当回收对象时,池分配器的头指针指向回收的对象,这样就实现了“对象池”的分配和回收。如下图所示:
池分配器(PoolAllocator)的节点
template<typename T>
class PoolChunk
{
public:
T value;
PoolChunk<T>* nextChunk;
public:
PoolChunk()
{
nextChunk = nullptr;
}
~PoolChunk()
{
}
};
池分配器(PoolAllocator)的链表结构
在池分配器(PoolAllocator)的构造函数中进行了池内对象的预选分配,并以链表(list)节点连接的方式存储各个池对象
template<typename T>
class PoolAllocator
{
private:
static const size_t POOL_DEFAULT_SIZE = 200; //默认数量大小
static const size_t DYNAMIC_INCRETA_SIZE = 50; //动态数量扩容大小, 当分配新的数量大于size时,size = size + DYNAMIC_INCRETA_SIZE
PoolChunk<T>* dataPtr;
PoolChunk<T>* headPtr;
size_t size;
size_t hasAllocateSize;
public:
PoolAllocator(size_t inPoolSize = POOL_DEFAULT_SIZE):
size(inPoolSize),
hasAllocateSize(0)
{
dataPtr = new PoolChunk<T>[size];
headPtr = dataPtr;
for (size_t index = 0; index < size; ++index)
{
dataPtr[index].nextChunk = std::addressof(dataPtr[index + 1]);
}
}
}
分配对象Allocate
比较注意的是,当池分配器分配对象时,链表内的节点取完了,下面池分配器会进行定量的扩容,这点倒是和std::vector的capacity机制类似
template<typename... Arguments>
T* (Arguments...args)
{
// 已经使用完池内的对象,扩大池的容量
if (nullptr == headPtr && hasAllocateSize >= size)
{
PoolChunk<T>* addDataPtr = new PoolChunk<T>[DYNAMIC_INCRETA_SIZE];
headPtr = addDataPtr;
for (size_t index = 0; index < DYNAMIC_INCRETA_SIZE - 1; ++index)
{
addDataPtr[index].nextChunk = std::addressof(addDataPtr[index + 1]);
}
}
T* newObject = new(std::addressof(headPtr->value)) T(std::forward<Arguments>(args)...);
headPtr = headPtr->nextChunk;
++hasAllocateSize;
return newObject;
}
回收对象Deallocate
void Deallocate(T* object)
{
--hasAllocateSize;
object->~T();
PoolChunk<T>* poolChunk = reinterpret_cast<PoolChunk<T>*>(object);
poolChunk->nextChunk = headPtr;
headPtr = poolChunk;
}
代码示例
class Object
{
private:
int value;
string uid;
public:
Object() = default;
Object(int inValue, string inUid):
value(inValue),
uid(inUid)
{
}
~Object()
{
printf("destruct");
}
void PrintInfo()
{
printf("value = %d, uid = %s\n", value, uid.c_str());
}
};
int main()
{
PoolAllocator<Object> objectPool;
Object* object1 = objectPool.Allocate(10, "object1");
Object* object2 = objectPool.Allocate(-223, "object2");
object1->PrintInfo();
object2->PrintInfo();
objectPool.Deallocate(object2);
objectPool.Deallocate(object1);
while (true)
{
object1 = objectPool.Allocate(10, "object1");
object2 = objectPool.Allocate(-223, "object2");
object1->PrintInfo();
object2->PrintInfo();
objectPool.Deallocate(object2);
objectPool.Deallocate(object1);
}
system("pause");
return 0;
}
参考资料
[1].《游戏引擎架构》5.2 内存管理
[2].Game Engine Tutorial [002] - Pool Allocator Theorie
[3].Game Engine Tutorial [003] - Pool Allocator Praxis