SGI STL的空间配置器alloc

这两天通过阅读SGI  STL源码,与《STL源码剖析》上侯捷对于源码的非常好的讲解与注释,me理解了C++关于内存管理的具体实现方法,觉得大有所益。。。整理一下,将实现空间配置器所用的策略大致记录下来。


1. STL容器简介

STL提供了很多泛型容器,如vector,list和map。程序员在使用这些容器时只需关心何时往容器内塞对象,而不用关心如何管理内存,需要用多少内存,这些STL容器极大地方便了C++程序的编写。例如可以通过以下语句创建一个vector,它实际上是一个按需增长的动态数组,其每个元素的类型为int整型:

stl::vector<int> array;


拥有这样一个动态数组后,用户只需要调用push_back方法往里面添加对象,而不需要考虑需要多少内存:

array.push_back(10);
    array.push_back(2);


vector会根据需要自动增长内存,在array退出其作用域时也会自动销毁占有的内存,这些对于用户来说是透明的,stl容器巧妙的避开了繁琐且易出错的内存管理工作。

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)的free_list组成,allocator会根据申请内存的大小(将这个大小round up成8的倍数)从对应的空闲块列表取表头块给用户。———————————————————————————————————————————


这里对如何将申请内存大小提升至8的倍数,做一个详细解释:(数学证明来自网络)

公式:   ((bytes)+_ALIGN- 1)&~(_ALIGN-1)

这是SGI STL 的空间配置器的第二级配置器

 template<bool threads ,int ints>

      class _default_alloc_template {...};

的私有函数

 static  size_t ROUND_UP( size_t  bytes ); 

的具体实现。 其中 enum{ _ALIGN=8 } ;


功能是:若请求的内存大小bytes不是8的倍数,则上调至8的倍数,比如如果申请的是4,就会返回8;请求的是31,返回32。。

解释:

  _ALIGN - 1 = 0b00000111

 ~(_ALIGN - 1) = 0b11111000

  ~(_ALIGN - 1)  进行 & 操作等价于去掉被8除的余数 。所以这个等式等价于

    [(byte+7)/8]*8

就等于这个数字向上取到8的倍数。用位运算是为了增加效率。


若想看到更为科学的数学方法证明,可以看我在知乎上面的提问:为什么这个公式可以实现将bytes上升至8的倍数?———————————————————————————————————————————

   实现时,allocator需要维护一个存储16个空闲块列表表头的数组free_list,数组元素i是一个指向块大小为8*(i+1)字节的空闲块列表的表头,一个指向内存池起始地址的指针start_free和一个指向结束地址的指针end_free。空闲块列表节点的结构如下:

union obj {

        union obj *free_list_link;   //存储下一个空闲块的地址

        char client_data[1];        //这个用法比较奇异,下面做详细解释

};


这个共用体的位置是在一个空闲内存块中的前四个字节,当这个内存块空闲时,它存储了下个空闲块,当这个内存块交给用户时,共用体所占的内存将会被用户数据覆盖。因此,allocator中的空闲块链表可以表示成:

obj* free_list[16];
—————————————————————————————————————————

对 共用体  obj 中的

 char client_data[1];
      做详细解释。

free_list_link指针存储了下一个空闲内存块的地址,按道理说不应该再有别的成员,但是事实上char client_data[1] 与这个并不冲突,它唯一可能的用处,就是被做取地址操作,得到当前内存块的地址:

因为公用同一段内存,所以 &client_data[1]  , 便可以得到当前内存块的地址~

可以说char client[1]这个数组在第二级配置器中没用实际意义,设计者的目的是为了构造一个柔性数组,不是有16个大小不同的内存块吗?从1字节-16字节。可以这么认为:free_list_link指针和数组client_data[1]分别是联合体在不同场合的代表。在free_list链表中,联合体结构只应用到了free_list_link指针,作用就是链接内存块,当内存块从free_list链表中返回给用户(动态申请)时,free_list_link将被遗弃,每个内存块的前4个字节存的就是在free_list表中该内存块的下一个内存块的地址,返回给用户后,这个free_list_link将被用户数据覆盖。而client数组的作用即是表示返回给用户的内存空间,其实也是没意义的,用户根本不会用联合体的成员访问自己申请的内存空间。那到底这个client数组有什么作用,可以说,对用户而言没任何作用,但对于设计者还是一种对精炼代码的极致追求吧!不懂柔性数组的可以参考:

http://www.cppblog.com/Dream5/articles/148386.html——————————————————————————————————————————

算法描述 :


1. 分配算法 allocate

allocator分配内存的算法如下:

算法: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算法建立空闲块列表并返回所需的内存地址;

        }

   }

}


2.算法: refill

输入:内存块的大小size

输出:建立空闲块链表并返回第一个可用的内存块地址

{

     调用chunk_alloc算法分配若干个大小为size的连续内存区域并返回起始地址chunk和成功分配的块数nobj;

    if(块数为1)直接返回chunk;

    否则

    {

         开始在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链入空闲列表块中;

    }

}


—————————————————————————————————————————

小结

STL中的内存分配器实际上是基于空闲链表(free_list)的分配策略,最主要的特点是通过组织16个空闲列表,对小对象的分配做了优化。

1)小对象的快速分配和释放。当一次性预先分配好一块固定大小的内存池后,对小于128字节的小块内存分配和释放的操作只是一些基本的指针操作,相比于直接调用malloc/free,开销小。

2)避免内存碎片的产生。零乱的内存碎片不仅会浪费内存空间,而且会给OS的内存管理造成压力。

3)尽可能最大化内存的利用率。当内存池尚有的空闲区域不足以分配所需的大小时,分配算法会将其链入到对应的空闲列表中,然后会尝试从空闲列表中寻找是否有合适大小的区域,


 




 
      










评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值