《STL源码剖析》学习笔记-第2章 空间配置器

1、SGI 特殊的空间配置器,std::alloc

SGI STL 的配置器与众不同,也与标准规范不同,其名称是 alloc 而非 allocator ,而且不接受任何参数。

我们所习惯的c++内存配置操作和释放操作如下:

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

这其中的new 操作符(new operator)包含两阶段操作:

1)调用operator new配置内存
(2)调用Foo::Foo( )构造函数构造对象内容。

delete操作符包含两阶段操作:

1)调用Foo::~Foo( )析构函数将对象析构。
(2)调用operator delete释放内存

注:如果只是进行空间分配操作,那么使用operator new就可以了,就好比C的malloc函数;如果已经分配好了空间,想在上面构造一个对象,那么可以使用placement new。

STL allocator 将这两阶段操作区分开来。内存配置操作由 alloc::allocate() 负责,内存释放操作由 alloc::deallocate() 负责;对象构造操作由 ::construct() 负责,对象析构操作由 ::destroy() 负责。

内存空间的配置/释放与对象内容的构造/析构,分别着落在<stl_alloc.h><stl_construct.h>这两个文件身上。此外还有一个文件<stl_uninitialized.h>,这里定义了一些全局函数,用来填充(fill)或复制(copy)大块内存数据,它们也都隶属于STL标准规范。

总结:

STL空间配置器主要分三个文件实现:
(1)<stl_construct.h> :这里定义了全局函数construct()和destroy(),负责对象的构造和析构。

(2)<stl_alloc.h>:文件中定义了一、二两级配置器,彼此合作,配置器名为alloc。

(3)<stl_uninitialized.h>:这里定义了一些全局函数,用来填充(fill)或复制(copy)大块内存数据,他们也都隶属于STL标准规范。

STL提供了五个全局函数用于处理空间,分别为:

1. construct 用于构造;
2. destroy 用于析构;
3. uninitialized_copy(first, last, result) 将[first,last)
范围内的对象复制到result处;
4. uninitiated_fill(first, last, X) 将[first,last)范围内的内
存用对象X的副本填充;
5. uninitiated_fill_n(first, n, X) 将first开始的n个连续的内存
空间用X的副本填充;

2、std::alloc如何以高效率淘汰前面的allocator的

简单来说,alloc主要在如下方面超越了allocator

1.通过内存池技术提升了分配效率:
2.对小内存频繁分配所可能造成的内存碎片问题的处理
3.对内存不足时的处理

3、两级空间配置器

SGI STL在<stl_alloc.h>中定义了两级配置器。第一级空间配置器使用malloc/free函数,当分配的空间大小超过128 bytes的时候使用第一级空间配置器;第二级空间配置器使用了内存池技术,当分配的空间大小小于128 bytes的时候,将使用第二级空间配置器。

当用户申请大区块时,它将其交予第一级配置器。当用户申请小区块时,将与内存池打交道,内存池通过自由链表(free-list)来管理小区块,当内存池不足时,会一次性向堆中申请足够大的空间。用户可以通过宏来控制使用哪一级配置器(默认为二级配置器)。

 static void* allocate(size_t __n)
  {
    void* __ret = 0;
    // 如果大于128 bytes,则使用第一级空间配置器
    if (__n > (size_t) _MAX_BYTES) {
      __ret = malloc_alloc::allocate(__n);
    }
    else {
      // 通过大小取得free-list数组下标,随后取得对应节点的指针
      // 相当于&_S_free_list[_S_freelist_index(__n)]
      _Obj* __STL_VOLATILE* __my_free_list
          = _S_free_list + _S_freelist_index(__n);
      _Obj* __RESTRICT __result = *__my_free_list;
      // 如果没有可用的节点,则通过_S_refill分配新的节点
      if (__result == 0)
        __ret = _S_refill(_S_round_up(__n));
      else {
        // 将当前节点移除,并当做结果返回给用户使用
        *__my_free_list = __result -> _M_free_list_link;
        __ret = __result;
      }
    }

    return __ret;
  };

大量分配小块的内存空间会带来问题:一是从运行库的堆管理器中取得的内存(如通过malloc获得),会有一部分空间用于存储管理信息,用于管理各个内存块,导致内存的使用率降低;二是过多的小块内存会带来内存碎片问题;采用合适的内存池技术可以避免这些问题

配置器除了负责配置,也负责回收。为了管理方便,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中有可用的区块,就直接拿来,如果没有,就准备通过refill为对应的free-list 重新填充空间。free-list的节点结构如下:

union obj
{
    union obj* free_list_link;
    char client_data[1];
};

这里使用union结构,是为了节省空间。
空间的回收,则是把内存重新加入到free-list对应的节点链表上去。

4、refill的机制

前面提到,如果free-list中没有可用的区块,就通过refill为对应的free-list 重新填充空间。此时refill进行了怎样的操作呢?

默认操作是通过_S_chunk_alloc从内存池中取得20个新的节点添加到free-list链表中,而如果内存池中的内存不够用,这时候能分多少就分多少节点,返回的相应的节点数。再万一内存池一个节点都提供不了,就给内存池新增空间,如果失败,再抛出bad_alloc异常。

template <bool __threads, int __inst>
void*
__default_alloc_template<__threads, __inst>::_S_refill(size_t __n)
{
    int __nobjs = 20;
    // 通过内存池分配内存,第二个参数为传引用方式
    char* __chunk = _S_chunk_alloc(__n, __nobjs);
    _Obj* __STL_VOLATILE* __my_free_list;
    _Obj* __result;
    _Obj* __current_obj;
    _Obj* __next_obj;
    int __i;
    // 如果只分配了一个节点,那么直接返回给用户就是了
    if (1 == __nobjs) return(__chunk);
    // 如果分配了不止一个节点,那么多余的我们要放到free-list里面去
    __my_free_list = _S_free_list + _S_freelist_index(__n);

    /* Build free list in chunk */
      __result = (_Obj*)__chunk;
      *__my_free_list = __next_obj = (_Obj*)(__chunk + __n);
      for (__i = 1; ; __i++) {
        __current_obj = __next_obj;
        __next_obj = (_Obj*)((char*)__next_obj + __n);
        if (__nobjs - 1 == __i) {
            // 最后一个节点的_M_free_list_link指针指向NULL,并跳出循环
            __current_obj -> _M_free_list_link = 0;
            break;
        } else {
            __current_obj -> _M_free_list_link = __next_obj;
        }
      }
    return(__result);
}

5、内存池

通过_S_chunk_alloc,从内存池中分配空间给free-list。_S_chunk_alloc的流程总结如下:

1、 内存池有足够大小的空间,则分配申请的空间;
2、 内存池没有足够大小的空间,但是至少还能分配一个节点的空间,则能分多少分多少;
3、 内存池一个节点都腾不出来,向系统的heap申请2倍于要求大小的空间,在
此之间,如果内存池剩余有空间,则放到free-list中去;
4、 如果向heap申请空间失败,那么只能看free-list中更大的节点是否有可
用空间了,有则用之,同时递归调用自身修正__nobjs;
5、 如果free-list也没有可用节点了,那么转向第一级空间配置器申请空间;
6、 再不行,第一级空间配置器就抛出bad_alloc异常。

如果有需求的话,内存池中会不断的通过malloc申请新的内存,最后内存池所拥有的内存也会越来越大,当然最后进程结束的时候,这些内存都会由操作系统收回。

部分参考自http://www.programlife.net/stl-allocator.html

你可能感兴趣的:(STL,空间配置器)