前言
在前面的游戏内存管理一之StackAllocator(堆栈分配器)和内存对齐和游戏内存管理二之PoolAllocator(池分配器)分别介绍了堆栈分配器和池管理器。堆栈分配器以堆栈的方式进行内存的分配和回收,并不是很灵活。而池分配器仅仅是针对一类对象,一类对象就是一个池,也就是我们存在N多种对象,就得用模板声明N种对象池,也是有点麻烦。所以希望实现一个比较通用的内存管理器,可以分配任意类型的对象,可以轻松的回收各种对象。
GeneratorAllocator(通用内存分配器)
GeneratorAllocator也是通过new或者malloc预先从系统内存的堆栈预先分配一大块连续的内存块。GeneratorAllocator的实现本质上来说模仿了“C++ New”的实现
GeneralAllocatorChunk
这里将一大段连续的内存块称为Chunk
//内存块
class GeneralAllocatorChunk
{
public:
byte* m_addr;
size_t m_size;
public:
GeneralAllocatorChunk():
m_addr(nullptr),
m_size(0)
{
}
GeneralAllocatorChunk(byte* addr, size_t size):
m_addr(addr),
m_size(size)
{
}
};
GeneratorAllocator
GeneratorAllocator是由N块内存块(GeneralAllocatorChunk)组成, 用一个Chunk数组表达,而且这个数组是按顺序(内存块的地址前后顺序)排列的,刚开始构造内存器的时候仅仅存在一个Chunk块
class GeneratorAllocator
{
private:
static const int DEFAULT_BYTE_SIZE = 1024;
byte* m_data;
size_t m_size;
TArray<GeneralAllocatorChunk, true> m_Chunks;
public:
GeneratorAllocator(size_t size = DEFAULT_BYTE_SIZE) :
m_size(size)
{
m_data = new byte[size];
m_Chunks.PushBack(GeneralAllocatorChunk(m_data, size));
}
GeneratorPointer
分配的对象以指针的形式的保存,里面存储了分配对象的初始地址和对象的数量
template<typename T>
class GeneratorPointer
{
public:
T* m_pdata;
size_t m_size;
public:
GeneratorPointer(T* data, size_t size):
m_pdata(data),
m_size(size)
{
}
}
分配对象的过程
在GeneratorAllocator的内存块数组中,寻找一个由足够大小的内存Chunk,然后由这个Chunk分配对象。当然如果分配的对象大小过大,导致Chunk数组里的每个Chunk都没有足够容量分配,则分配失效。(和C++ new 分配内存原理类似,经常所说的内存碎片过多导致分配失败也是这种道理)
template<typename T, typename... Args>
GeneratorPointer<T> AllocateObjects(size_t objectNum = 1, Args&&... args)
{
for (size_t index = 0; index < m_Chunks.GetNum(); ++index)
{
T* data = m_Chunks[index].AllocateObject<T>(objectNum, std::forward<Args>(args)...);
if (nullptr != data)
{
if (m_Chunks[index].m_size == 0)
{
m_Chunks.RemoveIndex(index);
}
return GeneratorPointer<T>(data, objectNum);
}
}
return GeneratorPointer<T>(nullptr, 0);
}
刚开始GeneratorAllocator对象初始化的时候仅仅有一个非常大的连续的Chunk
从内存Chunk中分配一段内存(褐色为已分配使用的内存,蓝色为未使用的内存)
template<typename T, typename... Args>
T* AllocateObject(size_t amountObjects = 1, Args&&...args)
{
byte* allocationAdress = (byte*)GetAlignAadress((void*)(m_addr + 1), sizeof(T));
size_t amountOfBytes = amountObjects * sizeof(T);
byte* newAdress = allocationAdress + amountOfBytes;
if (newAdress < m_addr + m_size)
{
//在新对齐的-1位置存储对齐偏移值
byte offset = (byte)(allocationAdress - m_addr);
allocationAdress[-1] = offset;
T* objectPointer = reinterpret_cast<T*>(allocationAdress);
m_size -= newAdress - m_addr;
m_addr = newAdress;
for (size_t index = 0; index < amountObjects; ++index)
{
new (std::addressof(objectPointer[index])) T(std::forward<Args>(args)...);
}
return objectPointer;
}
return nullptr;
}
连续分配几次不同大小的内存之后,最后剩下一小段未使用的内存(蓝色)
回收对象的过程
回收内存对象是不定顺序的,回收已分配的对象可能导致出现新的Chunk(连续的内存才能称为Chunk),如下面蓝色未分配的内存块数量增加1了。
当然如果回收的对象内存其前后地址存在相连的未分配内存Chunk,则形成一整块新的更大的Chunk
template<typename T>
void DeallocateObject(GeneratorPointer<T> pointer)
{
//析构pointer里的所有对象
for (size_t index = 0; index < pointer.m_size; ++index)
{
pointer[index].~T();
}
byte* bytePointer = reinterpret_cast<byte*>(pointer.GetPointer());
size_t amountOfBytes = sizeof(T) * pointer.m_size;
byte offset = bytePointer[-1];
//还原这个pointer代表的trunk的大小
GeneralAllocatorChunk deallocateTrunk(bytePointer - offset, amountOfBytes + offset);
GeneralAllocatorChunk* leftTrunkPtr = nullptr;
GeneralAllocatorChunk* rightTrunkPtr = nullptr;
m_Chunks.GetNeighbors(deallocateTrunk, leftTrunkPtr, rightTrunkPtr);
bool bMerge = false;
GeneralAllocatorChunk* deallocateTrunkPtr = &deallocateTrunk;
//判断左边是否相邻
if (nullptr != leftTrunkPtr)
{
if (leftTrunkPtr->touch(deallocateTrunk))
{
leftTrunkPtr->m_size += deallocateTrunk.m_size;
deallocateTrunkPtr = leftTrunkPtr;
bMerge = true;
}
}
//判断右边时候能合并
if (nullptr != rightTrunkPtr)
{
if (rightTrunkPtr->touch(deallocateTrunk))
{
//若和左边已经合并的情况
if (bMerge)
{
rightTrunkPtr->m_size += deallocateTrunk.m_size;
m_Chunks.RemoveItem(*rightTrunkPtr);
}
else
{
rightTrunkPtr->m_size += deallocateTrunkPtr->m_size;
rightTrunkPtr->m_addr = deallocateTrunkPtr->m_addr;
bMerge = true;
}
}
}
if (!bMerge)
{
m_Chunks.PushBack(deallocateTrunk);
}
}
GeneratorAllocator(通用分配器)和C++的new原理非常相似,滥用也会导致细小Chunk块过多,而无法继续分配内存。本质上GeneratorAllocator是就是预先分配一大段连续的内存,然后在C++软件层模拟new的分配和回收。
例子代码
class UObject
{
private:
int value;
string name;
public:
UObject():
value(10),
name(string("123"))
{
}
UObject(int inValue, string inName) :
value(inValue),
name(inName)
{
}
//拷贝构造
UObject(const UObject& other):
value(other.value),
name(other.name)
{
}
//赋值函数
UObject& operator = (const UObject& other)
{
if (this == &other)
{
return *this;
}
value = other.value;
name = other.name;
return *this;
}
//移动拷贝构造函数
UObject(UObject&& other):
value(other.value),
name(other.name)
{
}
//移动赋值函数
UObject& operator = (UObject&& other)
{
value = other.value;
name = other.name;
return *this;
}
void Print()
{
printf("value = %d\n", value);
printf("name = %s\n", name.c_str());
}
~UObject()
{
printf("uobject destruct\n");
}
};
int main()
{
GeneratorAllocator generatorAllocator;
while (true)
{
GeneratorPointer<UObject> object1 = generatorAllocator.AllocateObjects<UObject>(15, 10, "123");
for (size_t index = 0; index < 15; ++index)
{
object1[index].Print();
}
generatorAllocator.DeallocateObject(object1);
}
system("pause");
return 0;
}
参考资料
【1】Game Engine Tutorial [006] - General Purpose Allocator Theorie
【2】《游戏引擎架构》5.2章节内存管理