STL源码剖析---空间配置器

其中本文是在STL源码剖析的基础所总结的,大家有机会可以去看看,其中有一大部分是别人总结的,自己转载过来方便大家一同观看。

在STL中有一个allocator配置器但只是基层内存配置/释放行为(也就是::operator new和::operator delete)的一层薄薄得包装,没有考虑到任何效率上的强化。

而一般而言我们所习惯的C++内存配置操作和释放操作时这样的:

class Foo
{....} ;
Foo *pf = new Foo;//配置内存,然后构造对象
delete pf;//将对象析构,然后释放内存


这其中new内含两阶段操作:(1)调用::operator new 配置内存;(2)调用Foo::Foo()构造对象内容。delete 也包含两个阶段(1)调用Foo::~Foo()将对象析构(2)调用::operator delete释放内存。

其中在源码中内存配置器由alloc::allocator()负责,内存释放操作由alloc::deallocate()负责;对象构造操作由::construct()负责,对象析构操作由::destroy()负责。

STL配置器在<memory>中,其中分为:

<stl_construct.h> 定义全局函数construct()和destroy()负责对象的构造和析构。

<stl_alloc.h>这里定义一、二级配置器,彼此合作。

<stl_uninitialized.h>定义了一些全局函数,用来填充或复制大块内存数据。


下面就是其他人整理的:http://blog.csdn.net/hackbuteer1/article/details/7724534

看过STL空间配置器的源码,总结一下:
      1、STL空间配置器:主要分三个文件实现,stl_construct.h  这里定义了全局函数construct()和destroy(),负责对象的构造和析构。stl_alloc.h文件中定义了一、二两级配置器,彼此合作,配置器名为alloc. stl_uninitialized.h 这里定义了一些全局函数,用来填充(fill)或复制(copy)大块内存数据,他们也都隶属于STL标准规划。
      在stl_alloc.h中定义了两级配置器,主要思想是申请大块内存池,小块内存直接从内存池中申请,当不够用时再申请新的内存池,还有就是大块内存直接申请。当申请空间大于128字节时调用第一级配置器,第一级配置器没有用operator::new和operator::delete来申请空间,而是直接调用malloc/free和realloc,并且实现了类似c++中new-handler的机制。所谓c++ new handler机制是,你可以要求系统在内存配置需求无法被满足时,调用一个指定的函数。换句话说,一旦::operator::new无法完成任务,在丢出std::bad_alloc异常状态之前,会先调用由客端指定的处理例程,该处理例程通常称为new-handler.new-handler解决内存做法有特定的模式。SGI第一级配置器的allocate()和realloc都是在调用malloc和realloc不成功后,改调用oom_malloc()和oom_realloc(),后两者都有内循环,不断调用"内存不足处理例程",期望在某次调用之后,获得足够的内存而圆满完成任务。但如果“内存不足处理例程“并未被客端设定,oom_malloc()和oom_realloc便调用_THROW_BAD_ALLOC, 丢出bad_alloc异常信息,或利用exit(1)硬生生中止程序。
     在stl_alloc.h中定义的第二级配置器中,如果区块够大,超过128字节时,就移交给第一级配置器处理。当区块小于128字节时,则以内存池管理,此法又称为次层配置,每次配置一大块内存,并维护对应的自由链表(free-list)。下次若再有相同大小的内存需求,就直接从free-list中拔出。如果客端释还小额区块,就由配置器回收到free-lists中,另外,配置器除了负责配置,也负责回收。为了管理方便,SGI第二级配置器会主动将任何小额区块的内存需求量上调至8的倍数。并维护16个free-lists,各自管理大小分别为8,16,24,32,40,48,56,64,72,80,88,96,104, 112,120,128 字节的小额区块。当申请小于等于128字节时就会检查对应的free list,如果free-list中有可用的区块,就直接拿来,如果没有,就准备为对应的free-list 重新填充空间。新的空间将取自内存池,缺省取得20个新节点,如果内存池不足(还足以一个以上的节点),就返回的相应的节点数.如果当内存池中连一个节点大小都不够时,就申请新的内存池,大小为2*total_bytes+ROUND_UP(heap_size>>4),totoal_bytes 为申请的空间大小,ROUND_UP调整为8的倍数,heap_size为当前总申请内存池的大小。如果申请该内存池成功就把原来内存池中剩下的空间分配给适当的free-list.万一山穷水尽,整个system heap空间都不够了(以至无法为内存池注入源头活水),malloc()行动失败,就会四处寻找有无"尚有未用区块,且区块足够大 "之free lists.找到了就挖一块交出,找不到就调用第一级配置器。第一级配置器其实也是使用malloc来配置内存。但它有out-of-memory处理机制(类似new-handler机制),或许有机会释放其他的内存拿来此处使用。如果可以就成功,否则发出bad_alloc异常。
      2、STL的默认内存分配器
      隐藏在这些容器后的内存管理工作是通过STL提供的一个默认的allocator实现的。当然,用户也可以定制自己的allocator,只要实现allocator模板所定义的接口方法即可,然后通过将自定义的allocator作为模板参数传递给STL容器,创建一个使用自定义allocator的STL容器对象,如:
    stl::vector<int, UserDefinedAllocator> array;
      大多数情况下,STL默认的allocator就已经足够了。这个allocator是一个由两级分配器构成的内存管理器,当申请的内存大小大于128byte时,就启动第一级分配器通过malloc直接向系统的堆空间分配,如果申请的内存大小小于128byte时,就启动第二级分配器,从一个预先分配好的内存池中取一块内存交付给用户,这个内存池由16个不同大小(8的倍数,8~128byte)的空闲列表组成,allocator会根据申请内存的大小(将这个大小round up成8的倍数)从对应的空闲块列表取表头块给用户。
这种做法有两个优点:
     (1)小对象的快速分配。小对象是从内存池分配的,这个内存池是系统调用一次malloc分配一块足够大的区域给程序备用,当内存池耗尽时再向系统申请一块新的区域,整个过程类似于批发和零售,起先是由allocator向总经商批发一定量的货物,然后零售给用户,与每次都总经商要一个货物再零售给用户的过程相比,显然是快捷了。当然,这里的一个问题时,内存池会带来一些内存的浪费,比如当只需分配一个小对象时,为了这个小对象可能要申请一大块的内存池,但这个浪费还是值得的,况且这种情况在实际应用中也并不多见。
     (2)避免了内存碎片的生成。程序中的小对象的分配极易造成内存碎片,给操作系统的内存管理带来了很大压力,系统中碎片的增多不但会影响内存分配的速度,而且会极大地降低内存的利用率。以内存池组织小对象的内存,从系统的角度看,只是一大块内存池,看不到小对象内存的分配和释放。
实现时,allocator需要维护一个存储16个空闲块列表表头的数组free_list,数组元素i是一个指向块大小为8*(i+1)字节的空闲块列表的表头,一个指向内存池起始地址的指针start_free和一个指向结束地址的指针end_free。空闲块列表节点的结构如下:

  1. union obj  
  2. {  
  3.     union obj * free_list_link;  
  4.     char client_data[1];  
  5. };  
union obj
{
	union obj * free_list_link;
	char client_data[1];
};
      这个结构可以看做是从一个内存块中抠出4个字节大小来,当这个内存块空闲时,它存储了下个空闲块,当这个内存块交付给用户时,它存储的时用户的数据。因此,allocator中的空闲块链表可以表示成:
    obj* free_list[16];
      3、分配算法

  1. // 算法:allocate   
  2. // 输入:申请内存的大小size   
  3. // 输出:若分配成功,则返回一个内存的地址,否则返回NULL   
  4. {  
  5.     if(size 大于 128)  
  6.         启动第一级分配器直接调用malloc分配所需的内存并返回内存地址;  
  7.     else  
  8.     {  
  9.         将size向上round up成8的倍数并根据大小从free_list中取对应的表头free_list_head  
  10.         if(free_list_head 不为空)  
  11.         {  
  12.             从该列表中取下第一个空闲块并调整free_list,返回free_list_head  
  13.         }  
  14.         else  
  15.         {  
  16.             调用refill算法建立空闲块列表并返回所需的内存地址  
  17.         }  
  18.     }  
  19. }  
  20.   
  21.   
  22. // 算法:refill   
  23. // 输入:内存块的大小size   
  24. // 输出:建立空闲块链表并返回第一个可用的内存地址   
  25. {  
  26.     调用chunk_alloc算法分配若干个大小为size的连续内存区域并返回起始地址chunk和成功分配的块数nobj  
  27.     if(块数为1)  
  28.         直接返回 chunk;  
  29.     else  
  30.     {  
  31.         开始在chunk地址块中建立free_list  
  32.         根据size取free_list中对应的表头元素free_list_head   
  33.         将free_list_head 指向chunk中偏移起始地址为size的地址处,即free_list_head = (obj*)(chunk+size)  
  34.         再将整个chunk中剩下的nobj-1个内存块串联起来构成一个空闲列表  
  35.         返回chunk,即chunk中第一个空闲的内存块  
  36.     }  
  37. }  
  38.   
  39.   
  40. // 算法:chunk_alloc   
  41. // 输入:内存块的大小size,预分配的内存块数nobj(以引用传递)   
  42. // 输出:一块连续的内存区域的地址和该区域内可以容纳的内存块的块数   
  43. {  
  44.     计算总共所需的内存大小total_bytes  
  45.     if(内存池足以分配,即end_free-start_free >= total_bytes)  
  46.     {  
  47.         则更新start_free  
  48.         返回旧的start_free  
  49.     }  
  50.     else if(内存池不够分配nobj个内存块,但至少可以分配一个)  
  51.     {  
  52.         计算可以分配的内存块数并修改nobj  
  53.         更新start_free并返回原来的start_free  
  54.     }  
  55.     else     // 内存池连一个内存块都分配不了   
  56.     {  
  57.         先将内存池的内存块链入到对应的free_list中后  
  58.         调用malloc操作重新分配内存池,大小为2倍的total_bytes为附加量,start_free指向返回的内存地址  
  59.         if(分配不成功)  
  60.         {  
  61.             if(16个空闲列表中尚有空闲块)  
  62.                 尝试将16个空闲列表中空闲块回收到内存池中再调用chunk_alloc(size,nobj)  
  63.             else  
  64.                 调用第一级分配器尝试out of memory机制是否还有用  
  65.         }  
  66.         更新end_free为start_free+total_bytes,heap_size为2倍的total_bytes  
  67.         调用chunk_alloc(size,nobj)  
  68.     }  
  69. }  
  70.   
  71.   
  72. // 算法:deallocate   
  73. // 输入:需要释放的内存块地址p和大小size   
  74. {  
  75.     if(size 大于128字节)  
  76.         直接调用free(p)释放  
  77.     else  
  78.     {  
  79.         将size向上取8的倍数,并据此获取对应的空闲列表表头指针free_list_head  
  80.         调整free_list_head将p链入空闲列表块中  
  81.     }  
  82. }  
// 算法:allocate
// 输入:申请内存的大小size
// 输出:若分配成功,则返回一个内存的地址,否则返回NULL
{
	if(size 大于 128)
		启动第一级分配器直接调用malloc分配所需的内存并返回内存地址;
	else
	{
		将size向上round up成8的倍数并根据大小从free_list中取对应的表头free_list_head
		if(free_list_head 不为空)
		{
			从该列表中取下第一个空闲块并调整free_list,返回free_list_head
		}
		else
		{
			调用refill算法建立空闲块列表并返回所需的内存地址
		}
	}
}


// 算法:refill
// 输入:内存块的大小size
// 输出:建立空闲块链表并返回第一个可用的内存地址
{
	调用chunk_alloc算法分配若干个大小为size的连续内存区域并返回起始地址chunk和成功分配的块数nobj
	if(块数为1)
		直接返回 chunk;
	else
	{
		开始在chunk地址块中建立free_list
		根据size取free_list中对应的表头元素free_list_head 
		将free_list_head 指向chunk中偏移起始地址为size的地址处,即free_list_head = (obj*)(chunk+size)
		再将整个chunk中剩下的nobj-1个内存块串联起来构成一个空闲列表
		返回chunk,即chunk中第一个空闲的内存块
	}
}


// 算法:chunk_alloc
// 输入:内存块的大小size,预分配的内存块数nobj(以引用传递)
// 输出:一块连续的内存区域的地址和该区域内可以容纳的内存块的块数
{
	计算总共所需的内存大小total_bytes
	if(内存池足以分配,即end_free-start_free >= total_bytes)
	{
		则更新start_free
		返回旧的start_free
	}
	else if(内存池不够分配nobj个内存块,但至少可以分配一个)
	{
		计算可以分配的内存块数并修改nobj
		更新start_free并返回原来的start_free
	}
	else     // 内存池连一个内存块都分配不了
	{
		先将内存池的内存块链入到对应的free_list中后
		调用malloc操作重新分配内存池,大小为2倍的total_bytes为附加量,start_free指向返回的内存地址
		if(分配不成功)
		{
			if(16个空闲列表中尚有空闲块)
				尝试将16个空闲列表中空闲块回收到内存池中再调用chunk_alloc(size,nobj)
			else
				调用第一级分配器尝试out of memory机制是否还有用
		}
		更新end_free为start_free+total_bytes,heap_size为2倍的total_bytes
		调用chunk_alloc(size,nobj)
	}
}


// 算法:deallocate
// 输入:需要释放的内存块地址p和大小size
{
	if(size 大于128字节)
		直接调用free(p)释放
	else
	{
		将size向上取8的倍数,并据此获取对应的空闲列表表头指针free_list_head
		调整free_list_head将p链入空闲列表块中
	}
}
假设这样一个场景,free_list[2]已经指向了大小为24字节的空闲块链表,如图1所示,当用户向allocator申请21字节大小的内存块时,allocaotr会首先检查free_list[2]并将free_list[2]所指的内存块分配给用户,然后将表头指向下一个可用的空闲块,如图2所示。注意,当内存块在链表上是,前4个字节是用作指向下一个空闲块,当分配给用户时,它是一块普通的内存区。

                        图1 某时刻allocator的状态

                                  图2 分配24字节大小的内存块
4、小结
STL中的内存分配器实际上是基于空闲列表(free list)的分配策略,最主要的特点是通过组织16个空闲列表,对小对象的分配做了优化。
1)小对象的快速分配和释放。当一次性预先分配好一块固定大小的内存池后,对小于128字节的小块内存分配和释放的操作只是一些基本的指针操作,相比于直接调用malloc/free,开销小。
2)避免内存碎片的产生。零乱的内存碎片不仅会浪费内存空间,而且会给OS的内存管理造成压力。
3)尽可能最大化内存的利用率。当内存池尚有的空闲区域不足以分配所需的大小时,分配算法会将其链入到对应的空闲列表中,然后会尝试从空闲列表中寻找是否有合适大小的区域,
但是,这种内存分配器局限于STL容器中使用,并不适合一个通用的内存分配。因为它要求在释放一个内存块时,必须提供这个内存块的大小,以便确定回收到哪个free list中,而STL容器是知道它所需分配的对象大小的,比如上述:
    stl::vector<int> array;
array是知道它需要分配的对象大小为sizeof(int)。一个通用的内存分配器是不需要知道待释放内存的大小的,类似于free(p)。



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值