游戏内存管理三之GeneratorAllocator(通用的内存分配器)

前言

在前面的游戏内存管理一之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章节内存管理

 

源码链接

https://download.csdn.net/download/qq_29523119/11984256

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值