C++STL详解二:萃取器与分配器

C++STL详解二:萃取器与分配器


前言

之前的文章中讨论了STL组成的六大部件,在本文中,就详细的分析一下分配器Allocator。我会用一个简易的分配器来加深对于分配器工作原理的了解。

但是在真正的开始讨论分配器之前,我们还需要知道一种在标准的STL库中很常见的编程技法:萃取器(Traits)。为了方便对于分配器本体原理的认识,我在后边的例子中,将不会使用萃取器,但我会首先讲述萃取器的原理和作用。


一、STL中常用的速率优化技法:萃取器(Traits)

1.什么是Traits

顾名思义,萃取器是一定要从某件事物中提取出某些东西的。在STL中,Traits被广泛的用于泛型算法的优化上,他通过萃取出object的某些属性,对拥有特殊属性的object进行函数模板的偏特化,进而提高算法的效率。

例如函数copy( ):
当我们需要复制一对迭代器之间的内容时,可能会出现多种情况:

  • 若这对迭代器指向内嵌数据类型时,我们可以使用memmove( )去实现,效率相当高。
  • 若这对迭代器指向某一个class时,我们就可能需要先为其分配空间,再去调用构造函数。

萃取器在这里面起到的作用就是用来提取出迭代器的类型,确定需要使用哪一种方法。

简单的说,traits就是用来提取出object的某一种属性的一个object


2.Traits的原理

如果看过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本身,从而取得对应的属性。

在这里插入图片描述

3.萃取器的大小

有了前面的代码,我们可以推想到,一个traits有多大?
我们可以注意到,traits内部并没有数据成员,只有一些typedef,所以理论上,使用sizeof去测量一个traits的大小,理论上结果应该是0。但是由于实作上的一些问题,这个结果是1。


二、分配器(Allocator)

1.什么是分配器

分配器是容器的底层依靠,他为容器分配内存空间。也就是说,容器中的数据都是存在动态分配的内存中的,栈中的容器只含有一些用来维护这些内存空间的指针。

因为容器的这种特性,所以容器需要频繁的分配内存和回收内存。如果直接调用new或者malloc,其产生的空间和时间 上的开销也是相当大的,至于为什么这么说,请参考我之前的文章:
C++内存分配详解二:重载new的动作

正因为如此,容器使用分配器去为自己分配内存,降低分配内存时空间和时间 的开销。

标准分配器的动作以及源码我在之前的文章中也有讲过,这里就不在做赘述,有兴趣可以参考我之前的文章:
C++内存分配详解四:std::alloc行为剖析
C++内存分配详解五:std::alloc源码剖析

下面我将展示另外一种简单的分配器 Loki Allocator。


2.Loki Allocator的行为分析

我们首先来看这个分配器的结构
本图片来自侯捷C++内存分配系列教程课件
在这里插入图片描述
最底层是Chunk,它用来管理被切成N等份的一大块内存

  • pData_:永远指向内存空间的开头
  • firstAvailableBlock_:记录下一个可以分配的内存块的下标
  • blocksAvailable_:记录当前chunk中可用的内存块的数量

如下图,在chunk中,通过malloc分配出的一大块的内存被当成是一个数组,并且每隔相同的距离,就会将当前内存空间当成是一个unsigned char,用来记录当前内存块的编号。这样,通过编号,整个chunk所管理的空间看起来就像是一个数组了。
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

  • chunks_:用来储存多个chunk。这些chunk每次分配出的内存块的大小都相同
  • allocChunk_:指向当前用来分配内存的chunk
  • deallocChunk_:指向上一次回收过内存的chunk

FixedAllocator的主要功能是判断哪一个chunk可以给出内存,如果没有可以给出内存的chunk,就再创造一个;同时也判断被归还的内存应该落在哪一个chunk之中。FixedAllocator也记录它所管理的chunk一次分配的内存块的大小。

分配内存时的判断很简单,判断allocChunk_指向的chunk是否有可以分配的内存,如果有就交由这个chunk分配,如果没有就再创建一个chunk。

回收内存才用就近判断原则。判断被归还的指针的字面值的大小是否在deallocChunk_所指的chunk的内存范围之中,如果是就交给当前chunk回收,如果不再就向前找一个或者向后找一个,直到找到可以归还的chunk。

同时,FixedAllocator也需要判断chunk中的内存是不是全部回收了,若一个chunk的内存被全部回收了,那么就将他放到最后,当再一次出现一个全回收的chunk,就将上一次记录的全回收的chunk归还给操作系统。这么做的目的是为了当再次需要创建chunk时,不需要再次向操作系统申请空间。


最顶层是SmallObjAllocator,这一层管理多个不同的fixedAllocator

  • pool_:用来储存多个FixedAllocator
  • pLastAlloc:指向上一次分配过内存的FixedAllocator
  • pLastDealloc:指向上一次回收过内存的FixedAllocator
  • maxObjectSize:记录可以通过当前分配器所分配的最大内存块的大小

这一层的主要作用是,当用户需要分配内存时,SmallObjAllocator判断是否存在一个fixedAllocator可以分配用户所需要的大小的内存,如果有,就由这个fixedAllocator去分配,如果没有,就创建一个fixedAllocator。

同时,再回收内存时,SmallObjAllocator也需要去判断被回收的内存属于哪一个fixedAllocator的管理中,并交给它去回收。

这个结构中存在一个maxObjectSize,也就是说,当我们申请的内存大于这个值时,再采用分配器的方法去分配,产生的效益已经微乎其微了,所以一般会去直接调用malloc分配内存。同理,若归还的内存大于这个值,也就说明不是由分配器分配的,将交给free去回收。


三、Loki Allocator 的简单实现

1.chunk的实现

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_ 。


2.FixedAllocator的实现

FixedAllocator需要完成的功能:

  • 判断是否存在可以分配的chunk,若没有则创建,并分配内存
  • 判断回收的内存属于哪一个chunk,并令对应chunk回收内存
  • 对全回收的chunk的处理

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);
}

3.SmallObjAllocator的实现

SmallObjAllocator需要完成的功能:

  • 判断需要分配的内存应该交由哪一个FixedAllocator,若不存在则创建
  • 判断被回收的内存应该落在哪一个FixedAllocator中

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);
}
  • 0
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值