软件架构设计之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派生对象就可以使用其好处。