软件架构设计之Utility模块——内存分配

一:前言

内存分配当然是对动态内存的分配(动态内存是在堆中分配的),使用动态内存的优点:

1.动态内存可以在不同的对象与函数之间共享。

2.动态分配的内存空间的大小可以在运行时确定。

关于内存的知识不想过多的解释,毕竟这是一篇注重设计与实现的文章,让我们把重点转移到:我们为什么要内存分配器,如何实现,对于多种实现,我们该如何抉择,实现后的效果如何等等一系列问题。

二:为什么要内存分配器

会有很多人说,内置的内存分配功能就已经足够使用了,还需要自己去分配内存吗?自己分配内存一般是指:先分配一大块(Chunk)内存,并在需要时将此内存按片(Block)进行分发。以下说说下内存分配器的好处:

1:可能会减少资源的开销,使用new分配内存时,系统还需额外的空间(4~32个bytes)来记录所分配的内存,在调用Delete释放的时候就可以依此来释放已分配的内存,这个额外的空间对于大对象来说所占比例太小,显得微不足道,而对于小对象就有点不可接受了。而即使是大对象而程序中大量的这种对象,额外的空间开销也是不容忽视的。使用内存分配器可以一次分配一大块内存可大大减少该额外空间。

2:这样我们可以申请连续的内存,提高Cache的命中率。

我们关注的重点是小对象的分配,大对象由于系统额外影响所占的比例有限因此不是我们考量的重点,但它仍然可以优化,所以这里均实作了出来。

三:内存分配器实现

这里我将设计两种内存分配(大对象内存分配,小对象内存分配),大对象与小对象如何区分呢?我就把它们以64bytes作为分界线,详细内容可参看《Modern C++ Design》的“小型对象分配技术”章节。这里的内存分配方式即采用该书中的小对象分配原理,并作了适当修改以适应大对象的分配及本软件设计。对于大对象分配又可划分两种:占住式(Hold),即分配了即不会归还给系统,Delete时将地址添加到空闲链表中,弹性式(Flex),即当大块均空闲时将该大块归还给系统。

先来说说公共部分吧。上面说了,公共部分就是大块内存(里面还有n个大小一样的区块),及管理这些大块内存的类叫FixedAllocator,大块内存叫Chunk,区块叫Block,下面就直接叫其英文名了。

1.    HTMemChunk实现

先看类定义:

// 块最大可分配的对象个数,限制为255(即一个unsigned char可表示的大小)
#ifndef MEM_BLOCKS_MAX
#define MEM_BLOCKS_MAX	255
#endif

#ifndef MEM_BLOCKSIZE_MAX
#define MEM_BLOCKSIZE_MAX 64
#endif

struct UTIL_API HTMemChunk
{
	// 按指定的区块大小(blockSize)和区块数(blocks)建立Chunk
	void Init(std::size_t blockSize, HT_UCHAR blocks);
	// blockSize要和Init函数里的blockSize一样。
	void* Allocate(std::size_t blockSize); 
	void Deallocate(HT_PVOID p, std::size_t blockSize);
	// 这个函数最重要是建立个block的索引号,充当链表。
	void Reset(std::size_t blockSize, HT_UCHAR blocks);
	void Release() { delete[] pData_; }
	HT_UCHAR* pData_;	// chunk首地址
	HT_UCHAR firstAvailableBlock_;	// 第一个可用block的索引号(在chunk中的位置号)
	HT_UCHAR blocksAvailable_;		// 可以block的总数
};

接着是类实现:

void HTMemChunk::Init( std::size_t blockSize, HT_UCHAR blocks )
{
	assert(blockSize > 0);
	assert(blocks > 0);
	// Overflow check,这应该很容易理解吧。
	assert((blockSize * blocks) / blockSize == blocks);
	pData_ = new HT_UCHAR[blockSize * blocks];
	Reset(blockSize, blocks);
}
void* HTMemChunk::Allocate( std::size_t blockSize )
{
	if (!blocksAvailable_) return 0;
	assert((firstAvailableBlock_ * blockSize) / blockSize == 
		firstAvailableBlock_);
	HT_UCHAR* pResult = pData_ + (firstAvailableBlock_ * blockSize);//得到可用block的位置
	firstAvailableBlock_ = *pResult;
	--blocksAvailable_;
	return pResult;
}
void HTMemChunk::Deallocate( HT_PVOID p, std::size_t blockSize )
{
	assert(p >= pData_);
	HT_UCHAR* toRelease = static_cast<HT_UCHAR*>(p);
	assert((toRelease - pData_) % blockSize == 0);
	*toRelease = firstAvailableBlock_; // 释放的block存储的是下一个可用block位置号
	firstAvailableBlock_ = static_cast<HT_UCHAR>(
		(toRelease - pData_) / blockSize); // 计算该block的位置号
	assert(firstAvailableBlock_ == (toRelease - pData_) / blockSize);
	++blocksAvailable_;
}
void HTMemChunk::Reset( std::size_t blockSize, HT_UCHAR blocks )
{
	assert(blockSize > 0);
	assert(blocks > 0);
	// Overflow check
	assert((blockSize * blocks) / blockSize == blocks);
	firstAvailableBlock_ = 0;
	blocksAvailable_ = blocks;
	HT_UCHAR i = 0;
	HT_UCHAR* p = pData_;
	// 建立索引号,记录下一个可用block在chunk中的位置
	for (; i != blocks; p += blockSize)
	{
		*p = ++i;
	}
}

 HTMemChunk类需要解释的是为什么将block数量限制在unsigned char大小了:假如我们使用较大的类型,例如unsigned short,我们将遭遇两个问题,

1.我们无法分配小于sizeof(unsigned short)的chunk,这是个小问题。

2.我们会遭遇齐位的问题,加入block为5bytes,如果将“指向如此一个5bytes的block”的指针转换为unsignedint,会造成未定义的行为,这是个大问题。

由于char的大小为1,所以无齐位问题。唯一的不好是,block的数量受到了限制(大多数系统中为255),不过这是可以接受的。

2.    HTFixedAllocator

同样先看类定义

class UTIL_API HTFixedAllocator
{
	typedef std::vector<HTMemChunk> Chunks;
public:	
	void Init(size_t blockSize, HT_UCHAR numBlocks);
	void* AllocateImpl(size_t size);
	void DeallocateImpl(void* p);

	size_t BlockSize() const
	{ return m_blockSize; }
	HT_BOOL operator<(size_t rhs) const
	{ return BlockSize() < rhs; }

private:
	size_t m_blockSize;
	HT_UCHAR m_numBlocks;
	HTMemChunk* m_pAllocChunk;	// 上次分配的Chunk
	HTMemChunk* m_pDeallocChunk; // 上次释放的Chunk
	Chunks m_chunks;
};

接着实现部分:

void HTFixedAllocator::Init( size_t blockSize, HT_UCHAR numBlocks )
{
	m_blockSize = blockSize;
	m_numBlocks = numBlocks;
	m_pAllocChunk = m_pDeallocChunk = HT_NULL;
}

void* HTFixedAllocator::AllocateImpl(size_t size)
{
	assert(size == m_blockSize);
	// 上次分配的Chunk有空闲Block,则直接从中取得内存
	if (m_pAllocChunk == 0 || m_pAllocChunk->blocksAvailable_ == 0)
	{
		// 遍列Chunk列表,找到空闲Block
		for (Chunks::iterator i = m_chunks.begin(); ; ++i)
		{
			if (i == m_chunks.end())	// 所有Chunk均满,新增大块内存
			{
				HTMemChunk newChunk;
				newChunk.Init(size, m_numBlocks);
				m_chunks.push_back(newChunk);
				m_pAllocChunk = &m_chunks.back();
				m_pDeallocChunk = &m_chunks.front();
				break;
			}
			if (i->blocksAvailable_ > 0)
			{
				m_pAllocChunk = &*i;
				break;
			}
		}
	}
	assert(m_pAllocChunk != 0);
	assert(m_pAllocChunk->blocksAvailable_ > 0);

	return m_pAllocChunk->Allocate(size);
}
void HTFixedAllocator::DeallocateImpl(void* p) 
{
	assert(!m_chunks.empty());
	assert(m_pDeallocChunk);
	const std::size_t chunkLength = m_numBlocks * m_blockSize;
	HTMemChunk* lo = m_pDeallocChunk;
	HTMemChunk* hi = m_pDeallocChunk + 1;
	HTMemChunk* loBound = &m_chunks.front();
	HTMemChunk* hiBound = &m_chunks.back() + 1;
	void* pTemp = p;

	// 找到需释放的内存首地址所在的Chunk,在m_pDeallocChunk附近查找
	if (hi == hiBound) hi = 0;
	for (;;)
	{
		if (lo)
		{
			if (pTemp >= lo->pData_ && pTemp < lo->pData_ + chunkLength)
			{
				m_pDeallocChunk = lo;
				break;
			}
			if (lo == loBound) lo = 0;
			else --lo;
		}

		if (hi)
		{
			if (pTemp >= hi->pData_ && pTemp < hi->pData_ + chunkLength)
			{
				m_pDeallocChunk = hi;
				break;
			}
			if (++hi == hiBound) hi = 0;
		}
	}
	assert(m_pDeallocChunk->pData_ <= pTemp);
	assert(m_pDeallocChunk->pData_ + m_numBlocks*m_blockSize > pTemp);
	m_pDeallocChunk->Deallocate(pTemp, m_blockSize);
	// Chunk为空,则将该Chunk归还系统
	if (m_pDeallocChunk->blocksAvailable_ == m_numBlocks)
	{
		HTMemChunk& lastChunk = m_chunks.back();
		if (&lastChunk == m_pDeallocChunk)
		{
			return;
		}
		if (lastChunk.blocksAvailable_ == m_numBlocks) // 存在两个空闲Chunk,则将m_chunks最后空闲Chunk归还给系统
		{
			lastChunk.Release();
			m_chunks.pop_back();
			m_pAllocChunk = m_pDeallocChunk;
		}
		else // 将空闲Chunk置换到尾部
		{
			std::swap(*m_pDeallocChunk, lastChunk);
			m_pAllocChunk = &m_chunks.back();
		}
	}
}

AllocateImpl()函数可以在常数时间内满足大多数分配请求,只偶尔会因为查找或添加新Chunk而变慢(需遍历查找空闲Chunk,或添加新Chunk)。现实世界中这种情况并不频繁,而且每一种分配器都有弱点。

DeallocateImpl()函数比较棘手,因为归还的时候我们不知道那个指针属于那个Chunk,方法是遍历m_chunks,检查是否落在pData_和pData_ + blockSize * blocks之间,如果是则将指针传给那个Chunk的Deallocate函数。问题是这很耗时,虽然分配速度很快(常数时间),归还确需耗用线性时间。我们需要一种方法来提高归还速度。这里采用的策略与分配相同的概念,m_pDeallocChunk记录上一次归还的Chunk,有任何归还动作就先检查该变量,如果这是一个错误的Chunk,才会去查找m_chunks。这时查找的方法是从m_pDeallocChunk附近开始查找合适的Chunk,即从m_pDeallocChunk所记录的Chunk在m_chunks中的位置向上或向下进行查找,这可以大大提高命中率。查找到了之后再更新m_pDeallocChunk。

归还时只有存在两个空Chunk,才释放其中一个Chunk,如果只有一个空Chunk,会被高效的置换到每m_chunks尾部。这样我们就能避免昂贵的erase动作,因为我们永远秩序删除最后一个Chunk。

3.     HTMemFlex(弹性式)

这是一个模板类,为每一个类型做一个分配器。看代码:

template <typename T, std::size_t NumBlocks = MEM_BLOCKS_MAX> 
class HTMemFlex : public HTFixedAllocator
{
	typedef T		value_type;
	typedef T*		stored_type;

	HTMemFlex() { Init(sizeof(value_type), NumBlocks); }
	~HTMemFlex() {}
	HTMemFlex(const HTMemFlex&);
	HTMemFlex& operator= (const HTMemFlex&);
public:
	static HTMemFlex& Instance() { static HTMemFlex obj; return obj;}

public:
	stored_type Allocate(size_t size)
	{
		stored_type pObj = (stored_type)AllocateImpl(size);
		return new (pObj)value_type();// 调用类的构造函数
	}

	void Deallocate(stored_type p, size_t num = 1)
	{
		p->~value_type();// 调用类的析构函数
		DeallocateImpl(p);
	}
};

4.     HTMemHold(占住式)

先看代码:

template <typename T, std::size_t NumBlocks = MEM_BLOCKS_MAX> 
class HTMemHold
{
	typedef T		value_type;
	typedef T*		stored_type;
	typedef std::vector<HTMemChunk> Chunks;			// 管理多个Chunk
	typedef std::vector<HT_PVOID> FreeBlocks;		// 空闲Block链表,释放时直接放到改变量中

	//HT_STATIC_ASSERT(NumBlocks <= MEM_MAX_BLOCKS)

	DECLARE_SINGLEMODE(HTMemHold)	// 这个宏为单令模式,以后不再叙述。

public:
	stored_type Allocate(size_t size)
	{
		assert(size == sizeof(value_type));
		HT_PVOID p = 0;
		if (m_freeBlocks.empty())	// 空闲Block链表为空
		{
			// 添加新chunk
			if (m_chunks.empty() || m_chunks.back().blocksAvailable_ <= 0)
			{
				HTMemChunk newChunk;
				newChunk.Init(sizeof(value_type), NumBlocks);
				m_chunks.push_back(newChunk);
			}

			p = m_chunks.back().Allocate(sizeof(value_type));
		}
		else 
		{
			p = m_freeBlocks.back();
			m_freeBlocks.pop_back();
		}

		return static_cast<stored_type>(p);
	}

	// 放入空闲Block链表即可
	void Deallocate(stored_type p, size_t num = 1) 
	{
		assert(num == 1);
		m_freeBlocks.push_back(p);
	}

	// 将已分配的所有Chunk归还给系统
	void Clear() 
	{
		for (Chunks::iterator iter = m_chunks.begin();
			iter != m_chunks.end(); ++iter)
		{
			iter->Release();
		}
		m_chunks.clear();
		m_freeBlocks.clear();
	}

private:
	Chunks m_chunks;
	FreeBlocks m_freeBlocks;
};

和HTMemFlex不同点在于释放时,直接放入m_freeBlocks缓冲区中,分配时先从中取,没有取到才添加新的。

5.     HTMemSmall(小对象分配器)

struct CompareFixedAllocatorSize
	: std::binary_function<const HTFixedAllocator &, const HTFixedAllocator &, bool>
{
	bool operator()(const HTFixedAllocator& lhs, const HTFixedAllocator& rhs) const
	{
		return lhs.BlockSize() < rhs.BlockSize();
	}
};

class UTIL_API HTSmallAllocator
{
public:
	HTSmallAllocator(std::size_t numBlocks, std::size_t maxBlockSize);

	void* Allocate(std::size_t numBytes);
	void Deallocate(void* p, std::size_t numBytes);

private:
	HTSmallAllocator(const HTSmallAllocator&);
	HTSmallAllocator& operator=(const HTSmallAllocator&);

	typedef std::vector<HTFixedAllocator> Pool;
	Pool m_pool;
	HTFixedAllocator* m_pLastAlloc;
	HTFixedAllocator* m_pLastDealloc;
	std::size_t m_numBlocks;
	std::size_t m_maxBlockSize;
};

template < std::size_t NumBlocks = MEM_BLOCKS_MAX, 
	std::size_t MaxBlockSize = MEM_BLOCKSIZE_MAX>
class HTMemSmall
{
	struct HTSmallObjAllocator : public HTSmallAllocator
	{
		HTSmallObjAllocator() 
			: HTSmallAllocator(NumBlocks, MaxBlockSize)
		{}

		static HTSmallObjAllocator& Instance()
		{
			return m_Obj;
		}

		static HTSmallObjAllocator m_Obj;
	};
public:
	static void* operator new(std::size_t size)
	{
#if (MEM_BLOCKSIZE_MAX != 0) && (MEM_BLOCKS_MAX != 0)
		/*typename YKThreadPolicy::Lock lock;
		(void)lock;*/

		return HTSmallObjAllocator::Instance().Allocate(size);
#else
		return ::operator new(size);
#endif
	}

	static void operator delete(void* p, std::size_t size)
	{
#if (MEM_BLOCKSIZE_MAX != 0) && (MEM_BLOCKS_MAX != 0)
		/*typename YKThreadPolicy::Lock lock;
		(void)lock;*/

		HTSmallObjAllocator::Instance().Deallocate(p, size);
#else
		::operator delete(p);
#endif
	}
	virtual ~HTMemSmall(void) {}
};

template </*template <class> class ThreadPolicy,*/
	std::size_t NumBlocks, 
	std::size_t MaxBlockSize>
	typename HTMemSmall</*ThreadPolicy, */NumBlocks, MaxBlockSize>::HTSmallObjAllocator
	HTMemSmall</*ThreadPolicy, */NumBlocks, MaxBlockSize>::HTSmallObjAllocator::m_Obj;
HTSmallAllocator::HTSmallAllocator( std::size_t numBlocks, std::size_t maxBlockSize )
	: m_pLastAlloc(0), m_pLastDealloc(0)
	, m_numBlocks(numBlocks), m_maxBlockSize(maxBlockSize)
{

}

void* HTSmallAllocator::Allocate( std::size_t numBytes )
{
	if (numBytes > m_maxBlockSize) return operator new(numBytes);

	if (m_pLastAlloc && m_pLastAlloc->BlockSize() == numBytes)
	{
		return m_pLastAlloc->AllocateImpl(numBytes);
	}

	HTFixedAllocator fixAlloc;
	fixAlloc.Init(numBytes, m_numBlocks);
	Pool::iterator i = std::lower_bound(m_pool.begin(), m_pool.end(), fixAlloc, 
		CompareFixedAllocatorSize());
	if (i == m_pool.end() || i->BlockSize() != numBytes)
	{
		i = m_pool.insert(i, fixAlloc);
		m_pLastDealloc = &*m_pool.begin();
	}
	m_pLastAlloc = &*i;
	return m_pLastAlloc->AllocateImpl(numBytes);
}

void HTSmallAllocator::Deallocate( void* p, std::size_t numBytes )
{
	if (numBytes > m_maxBlockSize) return operator delete(p);

	if (m_pLastDealloc && m_pLastDealloc->BlockSize() == numBytes)
	{
		m_pLastDealloc->DeallocateImpl(p);
		return;
	}

	HTFixedAllocator fixAlloc;
	fixAlloc.Init(numBytes, m_numBlocks);
	Pool::iterator i = std::lower_bound(m_pool.begin(), m_pool.end(), fixAlloc, 
		CompareFixedAllocatorSize());
	assert(i != m_pool.end());
	assert(i->BlockSize() == numBytes);
	m_pLastDealloc = &*i;
	m_pLastDealloc->DeallocateImpl(p);
}

这里的问题是找到相应blockSize的HTFixedAllocator,这里采用和HTFixedAllocator相同的策略,为了提高查找速度,我们将m_pool按blockSize大小排序。

HTMemSmall重载了系统提供的operatornew和operator delete。这样生成一个HTMemSmall派生对象就可以使用其好处。

四:测试结果

大家可以使用以上的代码进行测试,望大家将测试结果告知与我。我希望我们可以一起探讨一起互动……
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值