STL之路 - 空间适配器二

具备次配置力的SGI空间配置器

 SGI STL的配置器与标准规范不同,其名称是alloc,不接受任何参数(PS:SGI STL的每一个容器都已经指定其缺省的空间配置器为alloc,对于编码而言没有多大的困扰)

1、SGI有一个符合标准的allocator适配器,但效率不高,SGI并没有使用(因此注释写着有DO NOT USE THIS FILE)

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

 首先对于new算式和delete算式而言,实际上分为两个阶段

 new:①调用operator new配置内存;②调用构造函数构造对象

 delete:①调用析构函数将对象析构;②调用::operator delete释放内存

3、构造和析构基本工具:construct()和destroy()

 主要是这张图:
construct()接受一个指针p和一个初值value,至于这个函数内部代码的一些解释可以部分参照我的上一篇文章。
destroy()有两个版本,第一个版本接受一个指针,准备将该指针之所致之物析构掉;第二个版本接受first和last
两个迭代器,准备将[first, last)范围内的所有对象析构掉。这里就存在一个问题了,对于基本类型而言,多次重复
的调用析构函数是种损伤,因此就有了_type_traits<T>判断该类型的析构函数是否无关痛痒。是的话就啥也不做,
否的话,每经历一个对象就调用第一个版本的destroy(个人认为就是调用对象的析构函数,至于原因就是防止内存
泄漏,析构函数能够保证内存被完整释放)

4、空间的配置和释放,std::alloc
首先谈一下SGI的设计哲学(此处手动滑稽):
SGI是以malloc( )和free( )完成内存的配置和释放的,考虑到小型区块可能造成的内存破碎问题,SGI设计了双层级配置器

第一级适配器:直接使用malloc和free
第二级适配器:使用memory pool整理方式,分配内存,自身无法处理或是内存不够时会调用第一级
PS:界限为128bytes

SGI给alloc包装了一个接口simple_alloc使其符合ST标准,这个接口使配置单位从bytes转到单个元素大小,其它容器使用这个
simple_alloc 接口,里面的函数就是单纯的转调用





5、第一级适配器 __malloc_alloc_template剖析
(1)第一级适配器有以下几个部分组成:
 ①处理内存不足时使用的函数指针(静态),一个用于分配,一个再分配,一个错误处理函数
 ②接口,分配allocate,释放deallocate,再分配reallocate
 ③仿C++的set_new_handler()(PS:对这部分有兴趣的可以参照Effective C++ 第49条)

(2)内存处理函数:(至于几个接口,实质上主要工作是由内存处理函数完成的)
①static void (*__malloc_alloc_oom_handler)()
 内存异常处理函数,初值为0,有待客户端设定,这个很重要,对于其它两个函数处理来说,如果
这个没有被设定,程序将直接抛出bad_alloc异常,或是直接终止程序
②static void *oom_malloc(size_t)
 
注意: 这里使用了C++的new-handle模式,但是并不能直接使用C++的set_new_handler(),因为C++不支持realloc

③static void *oom_realloc(void *, size_t):过程与上面相同,只不过malloc变成了realloc

总结:第一级适配器直接使用malloc和free,同时对于内存分配异常时采用了new-handle模式



6、第二级适配器 __default_alloc_template剖析
 第二级适配器使用了内存池的机制,如果区块够小就采用第二级配置器,第二级配置器可以避免太多小额区块造成
内存的碎片以及配置时的额外负担(我的理解是,由于每一块都需要额外的开销记录内存的一些信息,所以小区快越多
空间 利用的效率就越低,而且如果小区块在空间上的位置分配的过于疏散,那么对于系统本身去维护这些小区快会造成
很大的负担),利用内存池的机制,尽可能的节省内存开销。
 次层配置:区块小于128bytes交由内存池管理



(1)内存池的原理:

①free-lists的节点结构:
union obj{
    union obj * free_list_link;
    char client_data[ 1 ];
}
 使用联合体union来节省内存从第一个字段来看,被视为指向下一个节点的指针;从第二阶段来看,可以被视为一个指针
指向实际的区块(我的理解是,在链表上的时候使用第一个字段,但是当它从链表上摘下来的时候,使用第二字段)

②内存池的结构:
 内存池实现原理是由16个链表构成的,这十六个链表free_list各自管理的大小不同,分别是8,16,24,,,128bytes的小额
区块,接下来我来看一下代码
首先是边界值:
# ifndef __SUNPRO_CC
    enum {__ALIGN = 8 };   //小型区块的上调边界
    enum {__MAX_BYTES = 128 };  //小型区块上限
    enum {__NFREELISTS = __MAX_BYTES / __ALIGN};  //free_lists个数
# endif
PS:这里的enum没有给出类型名,表示只打算使用常量而不创建枚举变量

下面来看一下内存池(位于_default_alloc_template内):
private:
# ifdef __SUNPRO_CC
    static obj * __VOLATILE free_list[];
        // Specifying a size results in duplicate def for 4.1
# else
    static obj * __VOLATILE free_list[__NFREELISTS];
# endif
这里的_VOLATILE的关键字就是volatile,volatile关键字告诉编译器不要进行过激的优化(具体内容自行百度)
 这里的free_list数组就是管理内存分配的16条链表(__NFREELISTS :上文出现了这个枚举变量,把它变成小写n_freelists,意思就很明确了),现在就是有一个指针数组,大小为16,每个代表的是一个obj的链表

给个草图吧:
free_list数组:
 
真草图。。。。。

数组中的一项:
(2)内存池的实现
 
数组的结构弄清楚了,那么现在还有几个问题:
 Ⅰ、 每个链表管理的项的大小是不同的,这是怎么做到的
 Ⅱ、 对于一个内存分配的请求,内存池是如何分配内存的,同时之后又是如何回收的
 Ⅲ、 内存池是如何实现对自身的管理的,如果自己内存小了,不能完成分配任务,怎么扩容,其次,怎么管理内存碎片的

下面解答这三个问题,这三个问题的一起解答

①前期准备工作
 首先对内存池的初始化:
template < bool threads, int inst >
__default_alloc_template < threads, inst > ::obj * __VOLATILE
__default_alloc_template < threads, inst > ::free_list[
# ifdef __SUNPRO_CC
    __NFREELISTS
# else
    __default_alloc_template < threads, inst > ::__NFREELISTS
# endif
] = { 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , };
// The 16 zeros are necessary to make version 4.1 of the SunPro
// compiler happy.  Otherwise it appears to allocate too little
// space for the array.
 
 来看一下几个对内存进行管理的函数
static size_t ROUND_UP ( size_t bytes)  //将分配空间上调至8的倍数,比如bytes为20,实际上分配大小会上升到24
static   size_t FREELIST_INDEX ( size_t bytes)  //找到对应的链表在数组中的下标,比如2-free_list[0], 30-free_list[3]
static void * refill ( size_t n);    //当所在链表的空间为零时,重新装填
static char * chunk_alloc ( size_t size, int & nobjs);  //重新装填的具体函数
 
②代码分析:
函数:ROUND_UP();
  static size_t ROUND_UP ( size_t bytes) {
        return (((bytes) + __ALIGN - 1 ) & ~ (__ALIGN - 1 ));
  }
模拟一遍,就知道这个函数可以实现,将任何比8的倍数小的bytes升级成离他最近的8的倍数

函数:FREELIST_INDEX(); 
  static   size_t FREELIST_INDEX ( size_t bytes) {
        return (((bytes) + __ALIGN - 1 ) / __ALIGN - 1 );
  }

找到与bytes最为接近的一个内存块(内存块大小是8的倍数)所对应的链表在内存池(数组)中的下标

人工模拟一遍内存操作过程:
Ⅰ、外部函数通过接口allocate申请内存
static void * allocate ( size_t n)
  {
    obj * __VOLATILE * my_free_list;
    obj * __RESTRICT result;

    if (n > ( size_t ) __MAX_BYTES) {  //大于128Bytes调用第一级适配器,否则由第二级适配器的内存池进行分配
        return ( malloc_alloc::allocate (n));
    }
    my_free_list = free_list + FREELIST_INDEX (n); //找到与当前size_t n大小最为接近的8的倍数所对应的->内存池中的链表
    //这个地方free_list是一个指针数组
 // Acquire the lock here with a constructor call. 
    // This ensures that it is released in exit or during stack
    // unwinding.
#       ifndef _NOTHREADS     //与线程相关,看不懂
        /*REFERENCED*/
        lock lock_instance;
#       endif
    result = * my_free_list;    //得到所在项obj的地址,因为my_free_list是一个双重指针,所以要解引用一层
    if (result == 0 ) {
        void * r = refill ( ROUND_UP (n)); //如果内存不够,就得重新装填
        return r;
    }
    * my_free_list = result -> free_list_link ; //链表头移至下一项
    return (result);
  };

执行过程见注释

template < bool threads, int inst >
void * __default_alloc_template < threads, inst > :: refill ( size_t n)
{
    int nobjs = 20 ;
    char * chunk = chunk_alloc (n, nobjs);  //重新申请内存
    obj * __VOLATILE * my_free_list;
    obj * result;
    obj * current_obj, * next_obj;
    int i;

    if ( 1 == nobjs) return (chunk);   //chunk_alloc第二个参数是按引用传递的,如如果申请的内存整好是一块且符合要求
    my_free_list = free_list + FREELIST_INDEX (n);

    /* Build free list in chunk */   
      result = (obj * )chunk;    //新得到的内存块有点大,就需要转换成链表
      * my_free_list = next_obj = (obj * )(chunk + n); //摘下当前需要的内存快,将后面的交给当前的链表
      for (i = 1 ; ; i ++ ) {      //余下块划分成链表节点,每个的大小是n(8的倍数)
        current_obj = next_obj;
        next_obj = (obj * )(( char * )next_obj + n);  //这个链表的大小指的的上限是19个,所以知道nobjs是干啥用的了
        if (nobjs - 1 == i) {     
            current_obj -> free_list_link = 0 ;
            break ;
        } else {
            current_obj -> free_list_link = next_obj;
        }
      }
    return (result);
}

template < bool threads, int inst >
char *
__default_alloc_template < threads, inst > :: chunk_alloc ( size_t size, int & nobjs)
{
    char * result;
    size_t total_bytes = size * nobjs;
    size_t bytes_left = end_free - start_free;

 //分配时,函数会先从回收的堆中分配,如果回收的堆的大小不够就得像系统申请内存了

    if (bytes_left >= total_bytes) { //剩余的内存够不够,这个内存是指内存池的回收heap
        result = start_free;   //对nobjs有疑问的看上面的注释
        start_free += total_bytes; //但是由于两者的度量方式不一样,heap的大小指的是byte数量,而size是8的倍数,所以是size * nobjs
        return (result);
    } else if (bytes_left >= size) { //size才是想要的byte大小,这是从回收的堆中分配的大小
        nobjs = bytes_left / size;
        total_bytes = size * nobjs; //后面的操作就是重新调整堆的大小
        result = start_free;
        start_free += total_bytes;
        return (result);
    } else {
        size_t bytes_to_get = 2 * total_bytes + ROUND_UP (heap_size >> 4 );
 //申请新内存的大小,至于为什么这样计算我就不知道了,有一点可以肯定的是,申请的大小一定是8的倍数
       
   // 尝试利用回收堆中的内存
        if (bytes_left > 0 ) {
            obj * __VOLATILE * my_free_list =
                        free_list + FREELIST_INDEX (bytes_left);

            ((obj * )start_free) -> free_list_link = * my_free_list;
            * my_free_list = (obj * )start_free;
        }
        start_free = ( char * ) malloc (bytes_to_get);
        if ( 0 == start_free) {  //分配失败,莫名蛋疼
            int i;
            obj * __VOLATILE * my_free_list, * p;
            // Try to make do with what we have.  That can't
            // hurt.  We do not try smaller requests, since that tends
            // to result in disaster on multi-process machines.
    //这里干的活,就是把新得到的内存划分成一个个大小为8的倍数区块,挂到各自链表上
            for (i = size; i <= __MAX_BYTES; i += __ALIGN) {
                my_free_list = free_list + FREELIST_INDEX (i);
                p = * my_free_list;
                if ( 0 != p) {  //找比当前块规模更大的链表,然后将链表的头结点交换给内存池,递归自己重新分配
                    * my_free_list = p -> free_list_link ;
                    start_free = ( char * )p;
                    end_free = start_free + i;
                    return ( chunk_alloc (size, nobjs));
                    // Any leftover piece will eventually make it to the
                    // right free list.
                }
            }
        end_free = 0 ;    // In case of exception.
            start_free = ( char * ) malloc_alloc::allocate (bytes_to_get);
            // This should either throw an
            // exception or remedy the situation.  Thus we assume it
            // succeeded.
        }
        heap_size += bytes_to_get;  //剩下的就好说了
        end_free = start_free + bytes_to_get;
        return ( chunk_alloc (size, nobjs));
    }
}

又来一张草图(具体写的时候情况肯定比这个要复杂很多,包括很多细节以及来自C++的秘境,这个等撸的时候再说)


至此关于内存分配的告一段落
可能有些地方讲解的不正确,欢迎交流以及指正。



Ⅱ、使用realloc申请:

template < bool threads, int inst >
void *
__default_alloc_template < threads, inst > :: reallocate ( void * p,
                                                    size_t old_sz,
                                                    size_t new_sz)
{
    void * result;
    size_t copy_sz;

    if (old_sz > ( size_t ) __MAX_BYTES && new_sz > ( size_t ) __MAX_BYTES) { //老规矩
        return ( realloc (p, new_sz));
    }
    if ( ROUND_UP (old_sz) == ROUND_UP (new_sz)) return (p);  //比较一下新老所要求块的大小是否一致,防止无所谓的调用
    result = allocate (new_sz);  
 //使用new_sz划分空间
    copy_sz = new_sz > old_sz ? old_sz : new_sz;  //保证新划分的空间不能比原来的要小
    memcpy (result, p, copy_sz);   //后面的一个是复制数据,然后是归还老内存
    deallocate (p, old_sz);
    return (result);
}



Ⅲ、使用deallocate释放

  static void deallocate ( void * p, size_t n)
  {
    obj * q = (obj * )p;
    obj * __VOLATILE * my_free_list;

    if (n > ( size_t ) __MAX_BYTES) {  //大于128Bytes,交由第一级适配器释放
        malloc_alloc::deallocate (p, n);
        return ;
    }
    my_free_list = free_list + FREELIST_INDEX (n); //否则的话就直接把这个内存块挂到对应的链表上,注意保持一致性,只能释放  //由allocate分配的内存,记住Byte的大小是8的倍数
    // acquire lock     
#       ifndef _NOTHREADS
        /*REFERENCED*/
        lock lock_instance;
#       endif /* _NOTHREADS */
    q -> free_list_link = * my_free_list;
    * my_free_list = q;
    // lock is released here
  }


过程就是这样
应该先好好看书,再去看代码。。。。。






猜你喜欢

转载自blog.csdn.net/qq_37925512/article/details/79162239