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表示链表节点结构:

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

union可以实现一物二用的效果,当节点所指的内存块是空暇块时,obj被视为一个指针,指向还有一个节点。

当节点已被分配时,被视为一个指针,指向实际区块。


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

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

代码例如以下:

  1.  /* n must be > 0      */  
  2.   static void * allocate(size_t n)  
  3.   {  
  4.     obj * __VOLATILE * my_free_list;  
  5.     obj * __RESTRICT result;  
  6.   
  7.     if (n > (size_t) __MAX_BYTES) {  
  8.         return(malloc_alloc::allocate(n));  
  9.     }  
  10.     my_free_list = free_list + FREELIST_INDEX(n);  
  11.     // Acquire the lock here with a constructor call.  
  12.     // This ensures that it is released in exit or during stack  
  13.     // unwinding.  
  14. #       ifndef _NOTHREADS  
  15.         /*REFERENCED*/  
  16.         lock lock_instance;  
  17. #       endif  
  18.     result = *my_free_list;  
  19.     if (result == 0) {  
  20.         void *r = refill(ROUND_UP(n));  
  21.         return r;  
  22.     }  
  23.     *my_free_list = result -> free_list_link;  
  24.     return (result);  
  25.   };  
这里须要注意的是,每次都是从相应的free list的头部取出可用的内存块。
图演示样例如以下:


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


refill()-为free list填充空间

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

缺省状况下取得20个新区块。可是假设内存池空间不够。取得的节点数就有可能小于20.以下是SGI STL中的源码:

  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.   
  14.     if (1 == nobjs) return(chunk);  
  15.     my_free_list = free_list + FREELIST_INDEX(n);  
  16.   
  17.     /* Build free list in chunk */  
  18.       result = (obj *)chunk;  
  19.       *my_free_list = next_obj = (obj *)(chunk + n);  
  20.       for (i = 1; ; i++) {//将各节点串接起来(注意,索引为0的返回给客端使用)  
  21.         current_obj = next_obj;  
  22.         next_obj = (obj *)((char *)next_obj + n);  
  23.         if (nobjs - 1 == i) {  
  24.             current_obj -> free_list_link = 0;  
  25.             break;  
  26.         } else {  
  27.             current_obj -> free_list_link = next_obj;  
  28.         }  
  29.       }  
  30.     return(result);  
  31. }  


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异常。

源码例如以下:

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

  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.   
  7.     if (n > (size_t) __MAX_BYTES) {  
  8.         malloc_alloc::deallocate(p, n);  
  9.         return;  
  10.     }  
  11.     my_free_list = free_list + FREELIST_INDEX(n);  
  12.     // acquire lock  
  13. #       ifndef _NOTHREADS  
  14.         /*REFERENCED*/  
  15.         lock lock_instance;  
  16. #       endif /* _NOTHREADS */  
  17.     q -> free_list_link = *my_free_list;  
  18.     *my_free_list = q;  
  19.     // lock is released here  
  20.   }  

写在后面:

       为什么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是为了下一层更好的使用。

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

如果程序以開始。client就调用chun_alloc(32,20),于是malloc配置40个(2倍的空间)32bytes区块。当中1个叫出来让用户使用,另外19个交给free_list[3]维护,剩余20个留给内存池,接下来client调用chunk_alloc(64,20),此时free_list[7]空空如也。必须向内存池要求支持。内存池仅仅够供应(32*20)/64 =10个64bytes区块,就把这10个区块返回,第一个交给client。剩余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?



posted on 2017-05-13 21:13  yjbjingcha  阅读(177)  评论(0编辑  收藏  举报

导航