您的位置:首页 > 运维架构 > 网站架构

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

2013-03-17 16:04 267 查看

一:前言

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

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派生对象就可以使用其好处。

四:测试结果

大家可以使用以上的代码进行测试,望大家将测试结果告知与我。我希望我们可以一起探讨一起互动……
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: