Memory 3

内存池

  内存池就是像一个养鱼池一样的东西,我们需要鱼的时候就从池子里捞出来,鱼少了以后又往池子里补充鱼苗,也就是用来做这两件事的一个池子而已。
  我们进行内存分配依赖的new和delete关键字,或是底层的malloc和free函数,它们是一些通用的内存分配回收的系统调用方法。频繁使用的时候,系统调用总是要比自己写的简单函数要慢一些。并且想象一间让你放东西和取东西的空屋,每次放进去的东西均不允许移动,当你随机地放入并取出一些小物件以后,房间里假设还有20个不完全连续的空位,以及10个未使用过剩余的连续空位,这时候又拿来了一整个占20个空位的大件物品,虽然数一数房间还剩下30个空位,但是这个大物件需要连续的空间才能放,所以仍然放不下了,这些无法再利用的小空间就被叫做变成为了“内存碎片”。
  减少系统调用的时间消耗,以及减少内存碎片的产生,正是内存池的最重要的工作。要减少时间消耗,就要让内存分配的方法尽可能简单,减少内存碎片,就为同样大小的小对象分出几块专用的独立空间来统一做分配管理。
  下面有一个非常简单高效的内存池的代码示例,先大概说明一下:

  • 该内存池的空间被分为Chunk(一大块内存),Slot(单个对象空间),如上面讲的例子,Chunk就像是房间,Slot就是空位。
  • _chunk是以单链表储存的房间队列,_slot是以单链表储存的空位队列。
  • 每次新开房间就添加到_chunk队列中,每次归还的空位就添加到_slot空位队列中。
  • 当需要分配空间时,先看看_slot空位队列有没有可用空位,有就取出来用,没有就从_chunk房间里拿,房间如果没有空间了就开个新房间来补充空间。
  • 当归还空位时,直接将空位加到_slot空位队列中就可以。
template <class Item, int CHUNK_CAPACITY = 4096>// 4KB
class MemoryPool
{
#define ITEM_SIZE sizeof(Item)
public:
	MemoryPool() :
		_chunk(new Chunk()), //房间队列
		_slot(nullptr) //空位队列
	{ }
	~MemoryPool()
	{
		MemoryPool::deleteChunk(_chunk);
	}
	void* alloc()
	{
		if (_slot)
		{
			Slot* head = _slot; //取空位
			_slot = _slot->next;
			return (void*)head;
		}
		else
		{
			if (_chunk->size + ITEM_SIZE > CHUNK_CAPACITY)
			{
            	/* 开房,并往队列头添加房间 */
				_chunk = new Chunk(_chunk);
			}
			char* addr = _chunk->buffer + _chunk->size;
			_chunk->size += ITEM_SIZE; //从房间里取空间
			return (void*)addr;
		}
	}
	void free(void* addr)
	{
    	/* 往队列头添加归还的空间 */
		Slot* slot = (Slot*)addr;
		slot->next = _slot;
		_slot = slot;
	}
private:
	struct Slot
	{
		Slot* next;
	};
	struct Chunk
	{
		Chunk(Chunk* next = nullptr) :
			buffer(new char[CHUNK_CAPACITY]),
			size(0),
			next(next)
		{ }
		~Chunk() { delete[] buffer; }
		int size;
		char* buffer;
		Chunk* next;
	};
	Slot* _slot;
	Chunk* _chunk;
	void deleteChunk(Chunk* chunk)
	{
		if (chunk)
		{
        	/* 递归清理房间 */
			MemoryPool::deleteChunk(chunk->next);
			delete chunk;
		}
	}
};

  实际上我花过很多时间研究这个内存池实现可能存在的一个问题。那就是内存对齐分配的问题,说到内存对齐,确实是一个有点复杂的话题。简单说就是硬件分配内存的时候如果没有把对象的空间进行“对齐”地分配,那被分配的对象以后在访问时速度会比较慢,或是有的硬件直接不支持不对齐地分配对象内存。至于对象如何内存对齐是有一套固定规则的(详见各种百科啦)。在我们上面的这套实现中其实可以不用担心这个问题,因为C++标准中说用new char[]分配的内存,只要不是给对齐系数大于alignof(std::max_align_t)的对象使用,其他对象都是可以把这段分配的内存直接从首地址开始使用而不需要做额外的地址对齐再使用的。我们一般的游戏程序对象,不要使用基本数据类型以外的,偏门的会导致对象的对齐系数大于alignof(std::max_align_t)的成员变量,都可以使用这个内存池的实现来管理。
C++标准中说,对于new方法

The pointer returned shall be suitably aligned so that it can be converted to a pointer of any complete object type with a fundamental alignment requirement

  std::max_align_t is a complete object type with the strictest fundamental alignment.
  来源详见Stack Overflow上的讨论
  Dorothy中使用的内存池实现的完整版,还带有空闲的池内存回收的功能,以及一些其他的辅助方法,详情请看这里

标题目录