STL源码分析--第二级空间配置器

本文讲解SGI STL空间配置器的第二级配置器。

相比第一级配置器,第二级配置器多了一些机制,避免小额区块造成内存的碎片。不仅仅是碎片的问题,配置时的额外负担也是一个大问题。因为区块越小,额外负担所占的比例就越大。

额外负担是指动态分配内存块的时候,位于其头部的额外信息,包括记录内存块大小的信息以及内存保护区(判断是否越界)。要想了解详细信息,请参考MSVC或者其他malloc实现。

SGI STL第二级配置器具体实现思想

如下:

  1. 如果要分配的区块大于128bytes,则移交给第一级配置器处理。
  2. 如果要分配的区块小于128bytes,则以内存池管理(memory pool),又称之次层配置(sub-allocation):每次配置一大块内存,并维护对应的自由链表(free-list)。下次若有相同大小的内存需求,则直接从free-list中取。如果有小额区块被释放,则由配置器回收到free-list中。

下面详细节介绍内存池管理技术。

在第二级配置器中,小额区块内存需求大小都被上调至8的倍数,比如需要分配的大小是30bytes,就自动调整为32bytes。系统中总共维护16个free-lists,各自管理大小为8,16,...,128bytes的小额区块。

为了维护链表,需要额外的指针,为了避免造成另外一种额外的负担,这里采用了一种技术:用union表示链表节点结构:

[cpp] view plaincopyprint?

  1. union obj {
  2. union obj * free_list_link;//指向下一个节点
  3. char client_data[1];    /* The client sees this. */
  4. };

union能够实现一物二用的效果,当节点所指的内存块是空闲块时,obj被视为一个指针,指向另一个节点。当节点已被分配时,被视为一个指针,指向实际区块。

以下是第二级配置器总体实现代码概览:

[cpp] view plaincopyprint?

  1. template <bool threads, int inst>
  2. class __default_alloc_template {
  3. private:
  4. // 實際上我們應該使用 static const int x = N
  5. // 來取代 enum { x = N }, 但目前支援該性質的編譯器還不多。
  6. # ifndef __SUNPRO_CC
  7. enum {__ALIGN = 8};
  8. enum {__MAX_BYTES = 128};
  9. enum {__NFREELISTS = __MAX_BYTES/__ALIGN};
  10. # endif
  11. static size_t ROUND_UP(size_t bytes) {
  12. return (((bytes) + __ALIGN-1) & ~(__ALIGN - 1));
  13. }
  14. __PRIVATE:
  15. union obj {
  16. union obj * free_list_link;
  17. char client_data[1];    /* The client sees this. */
  18. };
  19. private:
  20. # ifdef __SUNPRO_CC
  21. static obj * __VOLATILE free_list[];
  22. // Specifying a size results in duplicate def for 4.1
  23. # else
  24. static obj * __VOLATILE free_list[__NFREELISTS];
  25. # endif
  26. static  size_t FREELIST_INDEX(size_t bytes) {
  27. return (((bytes) + __ALIGN-1)/__ALIGN - 1);
  28. }
  29. // Returns an object of size n, and optionally adds to size n free list.
  30. static void *refill(size_t n);
  31. // Allocates a chunk for nobjs of size "size".  nobjs may be reduced
  32. // if it is inconvenient to allocate the requested number.
  33. static char *chunk_alloc(size_t size, int &nobjs);
  34. // Chunk allocation state.
  35. static char *start_free;
  36. static char *end_free;
  37. static size_t heap_size;
  38. /* n must be > 0      */
  39. static void * allocate(size_t n){...}
  40. /* p may not be 0 */
  41. static void deallocate(void *p, size_t n){...}
  42. static void * reallocate(void *p, size_t old_sz, size_t new_sz);
  43. template <bool threads, int inst>
  44. char *__default_alloc_template<threads, inst>::start_free = 0;//内存池起始位置
  45. template <bool threads, int inst>
  46. char *__default_alloc_template<threads, inst>::end_free = 0;//内存池结束位置
  47. template <bool threads, int inst>
  48. size_t __default_alloc_template<threads, inst>::heap_size = 0;
  49. template <bool threads, int inst>
  50. __default_alloc_template<threads, inst>::obj * __VOLATILE
  51. __default_alloc_template<threads, inst> ::free_list[
  52. # ifdef __SUNPRO_CC
  53. __NFREELISTS
  54. # else
  55. __default_alloc_template<threads, inst>::__NFREELISTS
  56. # endif
  57. ] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, };

空间配置函数allocate()

具体实现如下:

  1. 要分配的区块小于128bytes,调用第一级配置器。
  2. 否则,向对应的free-list寻求帮助。
    • 对应的free list有可用的区块,直接拿过来用。
    • 如果没有可用的区块,调用函数refill()为free list重新填充空间。

代码如下:

[cpp] view plaincopyprint?

  1. /* n must be > 0      */
  2. static void * allocate(size_t n)
  3. {
  4. obj * __VOLATILE * my_free_list;
  5. obj * __RESTRICT result;
  6. if (n > (size_t) __MAX_BYTES) {
  7. return(malloc_alloc::allocate(n));
  8. }
  9. my_free_list = free_list + FREELIST_INDEX(n);
  10. // Acquire the lock here with a constructor call.
  11. // This ensures that it is released in exit or during stack
  12. // unwinding.
  13. #       ifndef _NOTHREADS
  14. /*REFERENCED*/
  15. lock lock_instance;
  16. #       endif
  17. result = *my_free_list;
  18. if (result == 0) {
  19. void *r = refill(ROUND_UP(n));
  20. return r;
  21. }
  22. *my_free_list = result -> free_list_link;
  23. return (result);
  24. };

这里需要注意的是,每次都是从对应的free list的头部取出可用的内存块。

图示如下:

图一 从free list取出空闲区块示意图

refill()-为free list填充空间

当发现对应的free list没有可用的空闲区块时,就需要调用此函数重新填充空间。新的空间将取自于内存池。内存池的管理后面会讲到。

缺省状况下取得20个新区块,但是如果内存池空间不够,取得的节点数就有可能小于20.下面是SGI STL中的源代码:

[cpp] view plaincopyprint?

  1. /* Returns an object of size n, and optionally adds to size n free list.*/
  2. /* We assume that n is properly aligned.                                */
  3. /* We hold the allocation lock.                                         */
  4. template <bool threads, int inst>
  5. void* __default_alloc_template<threads, inst>::refill(size_t n)
  6. {
  7. int nobjs = 20;
  8. char * chunk = chunk_alloc(n, nobjs);
  9. obj * __VOLATILE * my_free_list;
  10. obj * result;
  11. obj * current_obj, * next_obj;
  12. int i;
  13. if (1 == nobjs) return(chunk);
  14. my_free_list = free_list + FREELIST_INDEX(n);
  15. /* Build free list in chunk */
  16. result = (obj *)chunk;
  17. *my_free_list = next_obj = (obj *)(chunk + n);
  18. for (i = 1; ; i++) {//将各节点串接起来(注意,索引为0的返回给客端使用)
  19. current_obj = next_obj;
  20. next_obj = (obj *)((char *)next_obj + n);
  21. if (nobjs - 1 == i) {
  22. current_obj -> free_list_link = 0;
  23. break;
  24. } else {
  25. current_obj -> free_list_link = next_obj;
  26. }
  27. }
  28. return(result);
  29. }

chunk_alloc-从内存池中取空间供free list使用

具体实现思想如下:

  1. 内存池剩余空间完全满足20个区块的需求量,则直接取出对应大小的空间。
  2. 内存池剩余空间不能完全满足20个区块的需求量,但是足够供应一个及一个以上的区块,则取出能够满足条件的区块个数的空间。
  3. 内存池剩余空间不能满足一个区块的大小,则
    • 首先判断内存池中是否有残余零头内存空间,如果有则进行回收,将其编入free list。
    • 然后向heap申请空间,补充内存池。
      • heap空间满足,空间分配成功。
      • heap空间不足,malloc()调用失败。则
        • 搜寻适当的free list(适当的是指:尚有未用区块,并且区块足够大),调整以进行释放,将其编入内存池。然后递归调用chunk_alloc函数从内存池取空间供free list。
        • 搜寻free list释放空间也未能解决问题,这时候调用第一级配置器,利用out-of-memory机制尝试解决内存不足问题。如果可以就成功,否则排除bad_alloc异常。

源代码如下:

[cpp] view plaincopyprint?

  1. /* We allocate memory in large chunks in order to avoid fragmenting     */
  2. /* the malloc heap too much.                                            */
  3. /* We assume that size is properly aligned.                             */
  4. /* We hold the allocation lock.                                         */
  5. template <bool threads, int inst>
  6. char*
  7. __default_alloc_template<threads, inst>::chunk_alloc(size_t size, int& nobjs)
  8. {
  9. char * result;
  10. size_t total_bytes = size * nobjs;
  11. size_t bytes_left = end_free - start_free;
  12. if (bytes_left >= total_bytes) {
  13. result = start_free;
  14. start_free += total_bytes;
  15. return(result);
  16. } else if (bytes_left >= size) {
  17. nobjs = bytes_left/size;
  18. total_bytes = size * nobjs;
  19. result = start_free;
  20. start_free += total_bytes;
  21. return(result);
  22. } else {
  23. size_t bytes_to_get = 2 * total_bytes + ROUND_UP(heap_size >> 4);//注意此处申请的空间的大小
  24. // Try to make use of the left-over piece.
  25. if (bytes_left > 0) {
  26. obj * __VOLATILE * my_free_list =
  27. free_list + FREELIST_INDEX(bytes_left);
  28. ((obj *)start_free) -> free_list_link = *my_free_list;
  29. *my_free_list = (obj *)start_free;
  30. }
  31. start_free = (char *)malloc(bytes_to_get);
  32. if (0 == start_free) {
  33. int i;
  34. obj * __VOLATILE * my_free_list, *p;
  35. // Try to make do with what we have.  That can‘t
  36. // hurt.  We do not try smaller requests, since that tends
  37. // to result in disaster on multi-process machines.
  38. for (i = size; i <= __MAX_BYTES; i += __ALIGN) {
  39. my_free_list = free_list + FREELIST_INDEX(i);
  40. p = *my_free_list;
  41. if (0 != p) {
  42. *my_free_list = p -> free_list_link;
  43. start_free = (char *)p;
  44. end_free = start_free + i;
  45. return(chunk_alloc(size, nobjs));
  46. // Any leftover piece will eventually make it to the
  47. // right free list.
  48. }
  49. }
  50. end_free = 0;   // In case of exception.
  51. start_free = (char *)malloc_alloc::allocate(bytes_to_get);
  52. // This should either throw an
  53. // exception or remedy the situation.  Thus we assume it
  54. // succeeded.
  55. }
  56. heap_size += bytes_to_get;
  57. end_free = start_free + bytes_to_get;
  58. return(chunk_alloc(size, nobjs));
  59. }
  60. }

注意:从heap中配置内存时,配置的大小为需求量的两倍再加上一个随配置次数逐渐增加的附加量。

内存池实例演示:

图二 内存池实例演示

程序一开始,客户调用chunk_alloc(32,20),因为此时内存池和free list空间均不够,于是调用malloc从heap配置40个32bytes区块,其中一个供使用,另一个交给free_list[3]维护。剩余的20个留给内存池。接下来调用chunk_alloc(64,20), 此 时 free_list[7] 空空如也,必须向记忆池要求支持。记忆池只够供应  (32*20)/64=10 个
64bytes区块,就把这 10 个区块传回,第 1 个交给客端,余 9个由 free_list[7] 维护。此时记忆池全空。接下来再呼叫chunk_alloc(96, 20),此时 free_list[11] 空空如也,必须向记忆池要求支持,而记忆池此时也是空的,于是以malloc()配 置 40+n(附加量)个 96bytes 区块,其中第 1 个交出,另 19 个交给 free_list[11] 维护,余 20+n(附加量)个区块留给记忆池……。

万一山穷水尽,整个system heap 空间都不够了(以至无法为记忆池注入活水源 头),alloc()行动失败,chunk_alloc()就到处寻找有无可用区块, 且区块够大之free lists。找到的话就挖一块交出,找不到的话就调用第一级配 置器。第一级配置器其实也是使用malloc()来配置内存,但它有 out-of-memory 处理机制(类似 new-handler   机制),或许有机会释放其它的内存拿来此处使用。
如果可以,就成功,否则发出bad_alloc异常。

deallocate()-空间释放函数

  1. 如果需要回收的区块大于128bytes,则调用第一级配置器。
  2. 如果需要回收的区块小于128bytes,找到对应的free -list,将区块回收。注意是将区块放入free -list的头部。

SGI STL源代码:

[cpp] view plaincopyprint?

  1. /* p may not be 0 */
  2. static void deallocate(void *p, size_t n)
  3. {
  4. obj *q = (obj *)p;
  5. obj * __VOLATILE * my_free_list;
  6. if (n > (size_t) __MAX_BYTES) {
  7. malloc_alloc::deallocate(p, n);
  8. return;
  9. }
  10. my_free_list = free_list + FREELIST_INDEX(n);
  11. // acquire lock
  12. #       ifndef _NOTHREADS
  13. /*REFERENCED*/
  14. lock lock_instance;
  15. #       endif /* _NOTHREADS */
  16. q -> free_list_link = *my_free_list;
  17. *my_free_list = q;
  18. // lock is released here
  19. }

写在后面:

为什么SLT要自己定制空间配置器?为了效率,如果不进行处理的话,在用户使用时随意调用系统调用,是很费时间。SLT中空间配置器为什么分为两层?在平常使用的时候,都是调用new或者delete来进行初始化和删除heap空间,但是在调用new的时候,首先是分配空间,然后调用构造函数进行空间的初始化,在调用delete时,首先是调用对象的析构函数然后再delete空间,这种操作对于STL中有些时候是不合适的,所以SLT空间配置器使用了两极的初始化。要明白SGI标准的空间配置器std::allocator和SGI特殊的空间配置器std:;alloc是有区别的,这里说的都是后者

使用的策略如下:如果用户申请的空间大于128bytes,那么就调用第一级的空间配置器,如果小于128bytes,那么就使用memory pool来进行处理,在用户使用的时候,都是使用再次封装好的了simple_alloc模板类,这里着重说使用空间小于128时,使用的是__default_alloc_template的情况,这里有一个数组free_list,数组中每一个元素都是一个链表,数组每个元素对应的都是逐渐增大的小的内存空间,如果申请空间时,在free_list相关位置可以找到合适的空间时就直接使用,在没有合适的空间时,就需要refill了,就是重新填充free_list,在refill中首先调用chunk_alloc尝试取得20个区块free_list的新节点,其实在chunk_alloc中是申请了2倍的空间,其中一半是给free_list,一半给memory
pool,给了free_list的部分在refill中整合各个节点空间,在memory pool是为了下一层更好的使用。

介绍一个书上的例子就知道空间怎是产生和使用了:

假设程序以开始,客户端就调用chun_alloc(32,20),于是malloc配置40个(2倍的空间)32bytes区块,其中1个叫出来让用户使用,另外19个交给free_list[3]维护,剩余20个留给内存池,接下来客户端调用chunk_alloc(64,20),此时free_list[7]空空如也。必须向内存池要求支持,内存池只够供应(32*20)/64 =10个64bytes区块,就把这10个区块返回,第一个交给客户端,剩余9个由free_list[7]维护,此时内存池全空。接下来在调用chunk_alloc(96,20),此时free_list[11]空空如也。必须向内存要求支持,而内存池此时也是空的,于是以malloc()配置40+n(附加量)个9bytes区块,其中第1个交出,另外19个交给free_list[11]维护。剩余20+n(附加量)个区块留给内存

参考资料:

Extreme memory usage for individual dynamic allocation

(C) how does a heap allocator handle
a 4-byte block header, while only returning addresses that are multiples of 8?

时间: 2024-08-02 14:06:00

STL源码分析--第二级空间配置器的相关文章

STL源码剖析(1):空间配置器

STL所有的操作对象(所有的数值)都存放在容器之内,容器需要分配空间以存放数据.为什么不说allocator是内存配置器而是空间配置器,因为空间不仅是内存,空间也可以是磁盘或其它辅助储存媒体.这里我们主要讨论内存配置. SGI STL每个容器缺省的空间配置器为alloc,如vector: template<class T, class Alloc = alloc> class vector{--} 一般而言,C++的内存配置和释放操作如下: class Object{--}; Object*

2.SGI STL第二级空间配置器__default_alloc_template的chunk_alloc函数

SGISTL默认使用二级空间配置器,当需要配置的区块大于128 bytes时SGI STL调用一级空间配置器,一级空间配置器的allocate函数直接使用malloc分配内存,deallocate函数直接使用free释放内存.当需要配置的区块小于128 bytes时SGI STL调用二级空间配置器. 相比于一级空间配置器简单粗暴的内存使用方法,二级空间配置器对内存的使用显得精细很多. 二级空间配置器的具体用法请看书,我就不抄书了,只对二级空间配置器中容易糊涂的地方写一下我的理解. 内存池和fre

SGI STL第二级空间配置器空间释放函数deallocate

union obj{ obj * free_list_link ; char client_data[1] ; }; __default_alloc_template拥有配置器标准接口函数deallocate().该函数首先判断区块大小,大于128bytes就调用第一级配置器,小于128bytes就找出相应的free list将区块回收: <span style="font-size:18px;">//p is not 0/null static void dealloca

STL源码分析--空间配置器的底层实现 (二)

STL源码分析-空间配置器 空间配置器中门道 在STL中的容器里都是使用统一的空间配置器,空间配置器就是管理分配内存和销毁内存的.在STL将在heap空间创建一个对象分为两个步骤,第一是申请一块内存,第二是在这块内存中初始化一个对象.首先申请空间是由malloc提供,初始化一个对象时由constructor管理.销毁一个对象也是由两步骤完成,第一是销毁空间上的对象,第二是释放这块内存. 同时,STL的空间配置器分为两级内存,如果申请的内存空间大于128KB,那么就使用第一级空间配置,如果小于,那

stl源码分析之hash table

本文主要分析g++ stl中哈希表的实现方法.stl中,除了以红黑树为底层存储结构的map和set,还有用哈希表实现的hash_map和hash_set.map和set的查询时间是对数级的,而hash_map和hash_set更快,可以达到常数级,不过哈希表需要更多内存空间,属于以空间换时间的用法,而且选择一个好的哈希函数也不那么容易. 一. 哈希表基本概念 哈希表,又名散列表,是根据关键字直接访问内存的数据结构.通过哈希函数,将键值映射转换成数组中的位置,就可以在O(1)的时间内访问到数据.举

stl源码分析之vector

上篇简单介绍了gcc4.8提供的几种allocator的实现方法和作用,这是所有stl组件的基础,容器必须通过allocator申请分配内存和释放内存,至于底层是直接分配释放内存还是使用内存池等方法就不是组件需要考虑的事情.这篇文章开始分析gcc4.8 stl的容器源码实现.stl的容器分为序列式容器和关联式容器,前者包括vector,list,queue以及stack等常用数据结构,后者包含了map,set以及hash table等比较高级的结构,本文就从使用最广泛也是最基础的vector开始

STL源码分析--仿函数 &amp; 模板的模板参数 &amp; 临时对象

STL源码分析-使用的一些特殊语法 关于泛型编程中用到的一些特殊语法,这些语法也适用于平常的模板编程 1.  类模板中使用静态成员变量 Static成员变量在类模板中并不是很特殊,同时这个变量不属于对象,属于实例化以后的这个类类型.每一个实例化对应一个static变量 2.  类模板中可以再有模板成员 3.  模板参数可以根据前一个模板参数而设定默认值 4.  类模板可以拥有非类型的模板参数 所谓非类型的模板参数就是内建型的模板参数 Template <class T,class Alloc =

STL 源码分析 # stl_iterator &amp; stl_iterator_base #

STL 源码分析 # stl_iterator_base && stl_iterator # 这里能很清楚的看到各个基础类型的继承关系 template <class _Tp, class _Distance> struct input_iterator { typedef input_iterator_tag iterator_category; typedef _Tp value_type; typedef _Distance difference_type; typede

stl源码分析之list

本文主要分析gcc4.8版本的stl list的源码实现,与vector的线性空间结构不同,list的节点是任意分散的,节点之间通过指针连接,好处是在任何位置插入删除元素都只需要常数时间,缺点是不能随机访问,查询复杂度是O(n),n为list中的元素个数.所以list非常适合应用与数据插入删除频繁的场景. 一. list节点 list节点定义如下, struct _List_node_base { _List_node_base* _M_next; _List_node_base* _M_pre