之前的文章中讨论了STL组成的六大部件,在本文中,就详细的分析一下分配器Allocator。我会用一个简易的分配器来加深对于分配器工作原理的了解。
但是在真正的开始讨论分配器之前,我们还需要知道一种在标准的STL库中很常见的编程技法:萃取器(Traits)。为了方便对于分配器本体原理的认识,我在后边的例子中,将不会使用萃取器,但我会首先讲述萃取器的原理和作用。
顾名思义,萃取器是一定要从某件事物中提取出某些东西的。在STL中,Traits被广泛的用于泛型算法的优化上,他通过萃取出object的某些属性,对拥有特殊属性的object进行函数模板的偏特化,进而提高算法的效率。
例如函数copy( ):
当我们需要复制一对迭代器之间的内容时,可能会出现多种情况:
萃取器在这里面起到的作用就是用来提取出迭代器的类型,确定需要使用哪一种方法。
简单的说,traits就是用来提取出object的某一种属性的一个object
如果看过STL的源代码,我们经常会看到,一个object的内部有很多的typedef,这些typedef就是用来回答Traits所提出的问题的,也就是表现某个boject自身的一些属性。
我在这里先截取出一段后面的文章中将出现的容器的代码:
template<typename T>
class _deque_iterator
{//迭代器
public:
//声明自己的属性,会带traits的提问
typedef random_access_iterator_tag iterator_category;//迭代器的类型
typedef T value_type;//迭代器指向的元素的类型
typedef T* pointer;//迭代器指向的元素的指针
typedef T& reference;//迭代器指向的元素的引用
typedef ptrdiff_t difference_type;//表示迭代器之间距离的元素的类型
......
......
}
这是deque的迭代器中的一个片段,通过typedef定义了五个属性,这也是STL标准中所要求的五个属性,通过这五个typedef,这个迭代器就可以回答泛型算法或者是萃取器对其的提问。
如果当某个泛型算法算法需要确定收到的迭代器的移动属性时,就可以通过
_deque_iterator< T >::iterator_category 来获取。
下边我们举一个迭代器的萃取器的例子:
template<class I>
struct iterator_traits {//泛化的迭代器萃取器
typedef typename I::value_type value_type;
......
};
template<class T>
struct iterator_traits<T*> {//偏特化的迭代器萃取器
typedef T value_type;
......
};
template<class T>
struct iterator_traits<const T*> {//偏特化的迭代器萃取器
typedef T value_type;
......
};
有了对于萃取器的定义之后,算法就可以通过:
iterator_traits< iter >: :value_type 来获取迭代器 iter的value_type的属性了。
有人可能回想,既然在定义迭代器之前,就可以直接询问,那为什么还需要迭代器呢?
要回答这个问题很简单。首先我们想,我们可以为什么样的object写typedef?我们只能为自定义的class写typedef。那么,对于内嵌的object(比如原生的指针)呢?我们不可能为编辑器自带的object写typedef(除非修改源代码),但是编辑器自带的object需要使用STL的泛型算法时就需要提供自己的typedef,这时就凸显了萃取器的作用。
STL中的萃取器通过模板的偏特化,为内嵌的object定义了属于他们自己的偏特化版本,只用来回复他们对应的问题;而对于自定义的class,就使用泛化的traits去询问class本身,从而取得对应的属性。
有了前面的代码,我们可以推想到,一个traits有多大?
我们可以注意到,traits内部并没有数据成员,只有一些typedef,所以理论上,使用sizeof去测量一个traits的大小,理论上结果应该是0。但是由于实作上的一些问题,这个结果是1。
分配器是容器的底层依靠,他为容器分配内存空间。也就是说,容器中的数据都是存在动态分配的内存中的,栈中的容器只含有一些用来维护这些内存空间的指针。
因为容器的这种特性,所以容器需要频繁的分配内存和回收内存。如果直接调用new或者malloc,其产生的空间和时间 上的开销也是相当大的,至于为什么这么说,请参考我之前的文章:
C++内存分配详解二:重载new的动作
正因为如此,容器使用分配器去为自己分配内存,降低分配内存时空间和时间 的开销。
标准分配器的动作以及源码我在之前的文章中也有讲过,这里就不在做赘述,有兴趣可以参考我之前的文章:
C++内存分配详解四:std::alloc行为剖析
C++内存分配详解五:std::alloc源码剖析
下面我将展示另外一种简单的分配器 Loki Allocator。
我们首先来看这个分配器的结构
本图片来自侯捷C++内存分配系列教程课件
最底层是Chunk,它用来管理被切成N等份的一大块内存
如下图,在chunk中,通过malloc分配出的一大块的内存被当成是一个数组,并且每隔相同的距离,就会将当前内存空间当成是一个unsigned char,用来记录当前内存块的编号。这样,通过编号,整个chunk所管理的空间看起来就像是一个数组了。
当需要分配内存时,就将pData_[firstAvailableBlock_]所对应的内存分配出去,并将firstAvailableBlock_的内容修改成pData_[firstAvailableBlock_]内的值,blocksAvailable_减1,例如:
初始时,firstAvailableBlock_为0,blocksAvailable_=64;此时申请分配了一次内存
于是就将指针 pData_ + (blockSize * FristAvailableBlock_) 也就是pData_ [0] 交付,并修改firstAvailableBlock_的值为已分配出的内存块中的值,此时firstAvailableBlock_为1,blocksAvailable_减一变成63;
当再次需要分配时,仍然重复,将pData_ [1]交付,firstAvailableBlock_改为2, blocksAvailable_改为62.
当回收内存时,就将被回收的内存中的内容改为当前的firstAvailableBlock_,并重新计算firstAvailableBlock_,使他指向被回收的那一块内存的编号,blocksAvailable_加1.
chunk之上是FixedAllocator,它用来管理多个具有同样单元大小的chunk
FixedAllocator的主要功能是判断哪一个chunk可以给出内存,如果没有可以给出内存的chunk,就再创造一个;同时也判断被归还的内存应该落在哪一个chunk之中。FixedAllocator也记录它所管理的chunk一次分配的内存块的大小。
分配内存时的判断很简单,判断allocChunk_指向的chunk是否有可以分配的内存,如果有就交由这个chunk分配,如果没有就再创建一个chunk。
回收内存才用就近判断原则。判断被归还的指针的字面值的大小是否在deallocChunk_所指的chunk的内存范围之中,如果是就交给当前chunk回收,如果不再就向前找一个或者向后找一个,直到找到可以归还的chunk。
同时,FixedAllocator也需要判断chunk中的内存是不是全部回收了,若一个chunk的内存被全部回收了,那么就将他放到最后,当再一次出现一个全回收的chunk,就将上一次记录的全回收的chunk归还给操作系统。这么做的目的是为了当再次需要创建chunk时,不需要再次向操作系统申请空间。
最顶层是SmallObjAllocator,这一层管理多个不同的fixedAllocator
这一层的主要作用是,当用户需要分配内存时,SmallObjAllocator判断是否存在一个fixedAllocator可以分配用户所需要的大小的内存,如果有,就由这个fixedAllocator去分配,如果没有,就创建一个fixedAllocator。
同时,再回收内存时,SmallObjAllocator也需要去判断被回收的内存属于哪一个fixedAllocator的管理中,并交给它去回收。
这个结构中存在一个maxObjectSize,也就是说,当我们申请的内存大于这个值时,再采用分配器的方法去分配,产生的效益已经微乎其微了,所以一般会去直接调用malloc分配内存。同理,若归还的内存大于这个值,也就说明不是由分配器分配的,将交给free去回收。
chunk需要完成的功能:
首先来看chunk的定义:
//***********************************
//chunk的定义
//***********************************
class chunk
{//内存块
public:
void Init(const size_t&, const unsigned char&);//初始化chunk
void Release() { if (pData_) delete (pData_); };//释放当前chunk的空间
void Reset(const size_t&, const unsigned char&);//将chunk中的空间“串成链表”
void* Allocate(const size_t&);// 分配内存
void deAllocate(void*, const size_t&);//回收内存
unsigned char* pData_;//指向实际操作的内存
unsigned char FristAvailableBlock_;//第一个可用的内存的编号
unsigned char AvalilableBlocks_;//当前块中的可用内存数
};
初始化函数,这个函数申请了一块空间,并交给另一个函数进行串联
void chunk::Init(const size_t& blockSize, const unsigned char& blocks)
{//初始化chunk
//申请blockSize * blocks bytes 大小的内存空间
pData_ = new unsigned char[blockSize * blocks];
Reset(blockSize, blocks);//初始化申请的内存空间
}
串联函数如下,他将刚刚分配的内存做成数组状,并填入索引值
void chunk::Reset(const size_t& blockSize, const unsigned char& blocks)
{//将chunk中的空间“串成链表”
FristAvailableBlock_ = 0;//设置chunk中第一个可用内存块的索引
AvalilableBlocks_ = blocks;//设置chunk中可用的内存块的个数
unsigned char i = 0;//每一个内存块的索引编号
unsigned char *p = pData_;
while (i != blocks)//设置数组中每一块所对应的索引
{
*p = ++i;//设置每一块对应的索引,这个索引是下一次分配的内存块的下标
//这里由于p是unsigned char* 所以对他++只会后移一个char的位置
//所以在移动p时,需要+= blockSize
p += blockSize;//移动p
}
}
接下来是最常使用的,内存的分配和回收:
void* chunk::Allocate(const size_t& blockSize)
{//分配内存,blockSize为所需分配的字节数
if (!AvalilableBlocks_) return 0;//若chunk中没有内存 返回0
unsigned char* res = pData_ + (blockSize * FristAvailableBlock_);//设置分配的指针
FristAvailableBlock_ = *res;//设置下一次分配的索引
AvalilableBlocks_--;//修改可用的内存块个数
return res;
}
void chunk::deAllocate(void *p, const size_t& blockSize)
{//归还p指向的blockSize字节的空间
unsigned char *pRelease = static_cast<unsigned char*>(p);//指针类型的强制转换
//设置归还的内存块中的索引
*pRelease = FristAvailableBlock_;
//令下一次分配的内存块的编号为此次归还的内存块的下标
FristAvailableBlock_ = static_cast<unsigned char>((pRelease - pData_) / blockSize);
AvalilableBlocks_++;//chunk中可用的内存块+1;
}
分配没什么好说的,上边再行为的地方已经说的很清楚了。
归还的时候,由于被归还的内存块应该优先被分配,所以我们再这里将FristAvailableBlock_ 设置为被归还的内存块的下标,原先的FristAvailableBlock_ 将会在被归还的内存被分配后再分配,所以被归还的内存块中的索引设置为原先的FristAvailableBlock_ 。
FixedAllocator需要完成的功能:
fixedAllocator的定义:
//***********************************
//FixedAllocator的定义
//***********************************
class FixedAllocator
{//管理多个被分成同样大小的内存块
public:
FixedAllocator();
FixedAllocator(size_t bytes = 0)
: blockSize_(bytes), allocChunk_(0), deallocChunk_(0), blockNum_(20) {};
void* Allocate();//分配内存
void DeAllocate(void *p);//回收内存
chunk* FindChunk(void* p);//查找需要回收的指针在哪一块内存中
void DoDeallocate(void *p);//回收内存,同时处理全回收
size_t blockSize_;//每一块的字节数
unsigned char blockNum_;//创建chunk时 一次性申请的内存块的个数
std::vector<chunk> chunks_;//管理chunk的vector
chunk* allocChunk_;//分配的头指针
chunk* deallocChunk_;//归还的头指针
};
分配内存:
void* FixedAllocator::Allocate()
{//分配内存
if (allocChunk_ == 0 || allocChunk_->AvalilableBlocks_ == 0)
{//若allocChunk_没绑定chunk或者绑定的chaunk没有可用内存
//从头开始寻找内存
for (auto i = chunks_.begin(); ; ++i)
{
if (i == chunks_.end())//若没有找到
{//新建一块chunk
chunks_.reserve(chunks_.size() + 1);//扩增
chunk newChunk;
newChunk.Init(blockSize_, blockNum_);//初始化chunk
chunks_.push_back(newChunk);//加入容器中
allocChunk_ = &chunks_.back();//修改正在分配的chunk位置的指针
//防止vector扩增导致vector在内存中的位置发生改变,deallocChunk指针的失效
deallocChunk_ = &chunks_.front();
break;
}
if (i->AvalilableBlocks_ > 0)//若找到了一块chunk中有可用内存
{
allocChunk_ = &*i;//调整正在分配的指针的位置
break;
}
}
}
return allocChunk_->Allocate(blockSize_);
}
找到对应的chunk 和回收内存
chunk* FixedAllocator::FindChunk(void *p)
{//找到指针对应的chun
const std::size_t chunkLength = blockNum_ * blockSize_;
chunk* lo = deallocChunk_;
chunk* hi = deallocChunk_ + 1;
chunk* loBound = &chunks_.front();
chunk* hiBound = &chunks_.back() + 1;
if (hi == hiBound) hi = 0;
for (;;)
{
if (lo)
{
if (p >= lo->pData_ && p < lo->pData_ + chunkLength)
{
return lo;
}
if (lo == loBound) lo = 0;
else --lo;
}
if (hi)
{
if (p >= hi->pData_ && p < hi->pData_ + chunkLength)
{
return hi;
}
if (++hi == hiBound) hi = 0;
}
}
}
void FixedAllocator::DoDeallocate(void *p)
{//真正回收内存的函数
//回收内存
deallocChunk_->deAllocate(p, blockSize_);
//如果已经全回收了
if (deallocChunk_->AvalilableBlocks_ == blockNum_)
{
chunk& lastChunk = chunks_.back();
//最后一个就是当前的 deallocChunk
if (&lastChunk == deallocChunk_)
{
//如果有两个全回收的chunk
if (chunks_.size() > 1 &&
deallocChunk_[-1].AvalilableBlocks_ == blockNum_)
{
//释放其中的一个
lastChunk.Release();
chunks_.pop_back();
allocChunk_ = deallocChunk_ = &chunks_.front();
}
return;
}
if (lastChunk.AvalilableBlocks_ == blockNum_)
{
//如果出现两个全回收的chunk,释放最后一个
lastChunk.Release();
chunks_.pop_back();
allocChunk_ = deallocChunk_;
}
else
{
//将空的chunk移至vector的结尾
std::swap(*deallocChunk_, lastChunk);
allocChunk_ = &chunks_.back();
}
}
}
void FixedAllocator::DeAllocate(void *p)
{//被上层结构调用的函数
deallocChunk_ = FindChunk(p);
DoDeallocate(p);
}
SmallObjAllocator需要完成的功能:
SmallObjAllocator的定义
//***********************************
//SmallObjAllocator的定义
//***********************************
class SmallObjAllocator
{
public:
SmallObjAllocator(size_t size = 4096, size_t max = 256) :chunkSize_(size), maxObjectSize_(max) {};
void * Allocate(const size_t& numBytes);//分配内存
void Deallocate(void* p, std::size_t numBytes);//回收内存
private:
std::vector<FixedAllocator> pool_;//管理不同大小的chunk链表的pool
FixedAllocator* pLastAlloc_;//最后一个可分配的fixed
FixedAllocator* pLastDealloc_;//最后一个可归还的fixed
size_t chunkSize_;
size_t maxObjectSize_;//最大可以分配的内存块的字节数
};
分配内存
void* SmallObjAllocator::Allocate(const size_t& numBytes)
{//分配内存
//如果需要分配的内存大于maxObjectSize_,就交给operator new
if (numBytes > maxObjectSize_) return operator new(numBytes);
//如果正在使用的FixedAllocator正好可以为本次请求分配
if (pLastAlloc_ && pLastAlloc_->blockSize_ == numBytes)
{
return pLastAlloc_->Allocate();
}
//找到第一个 >= numBytes 的位置
auto p = pool_.begin();
for (; p != pool_.end(); p++)
{
if (p->blockSize_ >= numBytes)
break;
}
//没找到相同的,就重新创建一个 FixedAllocator
if (p == pool_.end() || p->blockSize_ != numBytes)
{
p = pool_.insert(p, FixedAllocator(numBytes));
pLastDealloc_ = &*pool_.begin();
}
pLastAlloc_ = &*p;
return pLastAlloc_->Allocate();
}
回收内存
void SmallObjAllocator::Deallocate(void* p, std::size_t numBytes)
{//回收内存
if (numBytes > maxObjectSize_) return operator delete(p);
//如果当前可以归还的FixedAllocator正好可以为p服务
if (pLastDealloc_ && pLastDealloc_->blockSize_ == numBytes)
{
pLastDealloc_->DeAllocate(p);
return;
}
//寻找第一个满足的FixedAllocator
auto i = pool_.begin();
for (; i != pool_.end(); i++)
{
if (i->blockSize_ >= numBytes)
break;
}
//回收内存
assert(i != pool_.end());
assert(i->blockSize_ == numBytes);
pLastDealloc_ = &*i;
pLastDealloc_->DeAllocate(p);
}