• STL——空间的配置和释放std::alloc(第一级配置器和第二级配置器)


    1 空间的配置和释放,std::alloc

    对象构造前的空间配置和对象析构后的空间释放,由<stl_alloc.h>负责,SGI对此的设计哲学如下:

    • 向system heap要求空间
    • 考虑多线程状态
    • 考虑内存不足时的应变措施
    • 考虑过多“小型区块”可能造成的内存碎片问题

    C++的内存配置基本操作是::operator new(),内存释放基本操作是::operator delete().这两个全局函数相当于C的malloc()和free()函数。是的,正是如此,SGI正是以malloc()和free()完成内存的配置和释放。

    考虑到小型区别所可能造成的内存碎片问题。SGI设计了双层配置器,第一级配置器直接使用malloc()和free(),第二级配置器则视情况采用不同的策略;当配置区块超过128bytes时,视之为“足够大”,便调用第一级配置器;当配置区块小于128bytes时,视之为“过小”,为了降低额外负担,便采用复杂的memory pool整理方式,而不再求助于第一级配置器。整个设计究竟是开放第一级配置器或是同时开放第二级配置器,取决于_USE_MALLOC是否被定义:

    # ifdef __USE_MALLOC 
    ... 
    typedef __malloc_alloc_template<0> malloc_alloc; <span style="font-family:KaiTi_GB2312;">//令 alloc为第一级配置器</span>
    typedef malloc_alloc alloc; 
    # else 
    ... 
    //令 alloc 为第二级配置器 
    typedef __default_alloc_template<__NODE_ALLOCATOR_THREADS, 0> alloc; 
    #endif /* ! __USE_MALLOC */ 
    

    其中__malloc_alloc_template就是第一级配置器,__default_alloc_template就是第二级配置器。

    无论alloc被定义为第一级或者是第二级配置器,SGI还为它包装一个接口如下,使配置器的接口能够符合STL规格:

    template<class T, class Alloc>
    class simple_alloc {
    
    public:
        static T *allocate(size_t n)
                    { return 0 == n? 0 : (T*) Alloc::allocate(n * sizeof (T)); }
        static T *allocate(void)
                    { return (T*) Alloc::allocate(sizeof (T)); }
        static void deallocate(T *p, size_t n)
                    { if (0 != n) Alloc::deallocate(p, n * sizeof (T)); }
        static void deallocate(T *p)
                    { Alloc::deallocate(p, sizeof (T)); }
    };
    

    其内部四个成员函数其实都是弹单纯的转调用,调用传递给配置器(可能是第一级也可能是第二级)的成员函数。

    一、二级配置器的关系如下:

    图 第一级配置器和第二级配置器

    第一级和第二级配置器的包装接口和运用方式如下:

    2 第一级配置器__malloc_alloc_template剖析

    首先我们观察第一级配置器:

    #if 0 
    #   include <new> 
    #   define  __THROW_BAD_ALLOC throw bad_alloc 
    #elif !defined(__THROW_BAD_ALLOC) 
    #   include <iostream.h> 
    #   define  __THROW_BAD_ALLOC cerr << "out of memory" << endl; exit(1) 
    #endif 
     
    // malloc-based allocator. 通常比稍后介绍的 default alloc 速度慢, 
    //一般而言是 thread-safe,并且对于空间的运用比较高效(efficient)。 
    //以下是第一级配置器。 
    //注意,无「template 型别参数」。至于「非型别参数」inst,完全没派上用场。
    template <int inst>   
    class __malloc_alloc_template { 
     
    private: 
    //以下都是函式指标,所代表的函式将用来处理内存不足的情况。 
    // oom : out of memory. 
    static void *oom_malloc(size_t); 
    static void *oom_realloc(void *, size_t); 
    static void (* __malloc_alloc_oom_handler)(); 
     
    public: 
     
    static void * allocate(size_t n) 
    { 
        void  *result =malloc(n);//第一级配置器直接使用 malloc() 
        // 以下,无法满足需求时,改用 oom_malloc() 
        if (0 == result) result = oom_malloc(n); 
        return  result; 
    } 
    static void deallocate(void *p, size_t /* n */) 
    { 
    free(p); //第一级配置器直接使用 free() 
    } 
     
    static void * reallocate(void *p, size_t /* old_sz */, size_t new_sz) 
    { 
        void  *  result  =realloc(p, new_sz);//第一级配置器直接使用 rea
        // 以下,无法满足需求时,改用 oom_realloc() 
        if (0 == result) result = oom_realloc(p, new_sz); 
        return  result; 
    } 
     
    //以下模拟 C++的 set_new_handler(). 换句话说,你可以透过它, 
    //指定你自己的 out-of-memory handler 
    static void (* set_malloc_handler(void (*f)()))() 
    { 
        void  (*  old)()  =  __malloc_alloc_oom_handler; 
    __malloc_alloc_oom_handler = f; 
        return(old); 
    } 
    }; 
     
    // malloc_alloc out-of-memory handling 
    //初值为 0。有待客端设定。 
    template <int inst> 
    void (* __malloc_alloc_template<inst>::__malloc_alloc_oom_handler)() = 0; 
     
    template <int inst> 
    void * __malloc_alloc_template<inst>::oom_malloc(size_t n) 
    { 
        void  (* my_malloc_handler)(); 
        void  *result; 
     
        for (;;)  { 
     
    //不断尝试释放、配置、再释放、再配置… 
    my_malloc_handler = __malloc_alloc_oom_handler; 
            if  (0  ==  my_malloc_handler)  {  __THROW_BAD_ALLOC; } 
            (*my_malloc_handler)();//呼叫处理例程,企图释放内存。 
            result = malloc(n);  //再次尝试配置内存。 
            if  (result)  return(result); 
        } 
    } 
     
    template <int inst> 
    void * __malloc_alloc_template<inst>::oom_realloc(void *p, size_t n) 
    { 
        void  (* my_malloc_handler)(); 
        void  *result; 
           for (;;)  {  //不断尝试释放、配置、再释放、再配置… 
    my_malloc_handler = __malloc_alloc_oom_handler; 
            if  (0  ==  my_malloc_handler)  {  __THROW_BAD_ALLOC; } 
            (*my_malloc_handler)();//呼叫处理例程,企图释放内存。 
            result = realloc(p, n);//再次尝试配置内存。 
            if  (result)  return(result); 
        } 
    } 
     
    //注意,以下直接将参数 inst指定为 0。 
    typedef __malloc_alloc_template<0> malloc_alloc; 
     

     第一级配置器直接使用malloc(),free(),realloc()等C函数执行实际的内存配置、释放、重配置操作,并实现出类似C++ new handler机制。它有独特的out-of-memory内存处理机制:在抛出std::bad_alloc异常之前,调用内存不足处理例程尝试释放空间,如果用户没有定义相应的内存不足处理例程,那么还是会抛出异常。

    所谓C++ new handler机制是,你可以要求系统在内存配置要求无法被满足时,调用一个你所指定的函数。换句话说,一旦::operator new无法完成任务,在丢出std::bad_alloc异常状态之前,会先调用由客户端指定的处理例程,该处理例程通常即被称为new-handler。new-handler解决内存不足的做饭有特定的模式。

    请注意,SGI第一级配置器的allocate()和realloc()都是在调用malloc()和realloc()不成功后,该调用oom_malloc()和oom_realloc()。后两者都有内循环,不断调用“内存不足处理例程”,期望在某次调用之后,获得足够的内存而圆满完成任务。但如果“内存不足处理例程”并未被客户端设定,oom_malloc()和oom_realloc()便老实不客气地调用__THROW_BAD_ALLOC,丢出bad_alloc异常信息,或利用exit(1)硬生生中止程序。

    记住,设计“内存不足处理例程”是客端的责任,设定“内存不足处理例程”也是客端的责任。

    3 第二级配置器__default_alloc_template剖析

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

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

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

    下面是第二级配置器的部分实现内容:

    enum {__ALIGN=8}; //小型区块的上调上界
    enum {__MAX_BYTES=128};  //小型区块的上限
    enum {__NFREELISTS=__MAX_BYRES/__ALIGN};  //free-lists个数
    

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

    template <bool threads, int inst>
    class __default_alloc_template {
    
    private:
      // 實際上我們應該使用 static const int x = N
      // 來取代 enum { x = N }, 但目前支援該性質的編譯器還不多。
    # ifndef __SUNPRO_CC
        enum {__ALIGN = 8};
        enum {__MAX_BYTES = 128};
        enum {__NFREELISTS = __MAX_BYTES/__ALIGN};
    # endif
      static size_t ROUND_UP(size_t bytes) {
            return (((bytes) + __ALIGN-1) & ~(__ALIGN - 1));
      }
    __PRIVATE:
      union obj {
            union obj * free_list_link;
            char client_data[1];    /* The client sees this. */
      };
    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
      static  size_t FREELIST_INDEX(size_t bytes) {
            return (((bytes) + __ALIGN-1)/__ALIGN - 1);
      }
    
      // Returns an object of size n, and optionally adds to size n free list.
      static void *refill(size_t n);
      // Allocates a chunk for nobjs of size "size".  nobjs may be reduced
      // if it is inconvenient to allocate the requested number.
      static char *chunk_alloc(size_t size, int &nobjs);
    
      // Chunk allocation state.
      static char *start_free;
      static char *end_free;
      static size_t heap_size;
    
     /* n must be > 0      */
      static void * allocate(size_t n){...}
    
     /* p may not be 0 */
      static void deallocate(void *p, size_t n){...}
     static void * reallocate(void *p, size_t old_sz, size_t new_sz);
    
    template <bool threads, int inst>
    char *__default_alloc_template<threads, inst>::start_free = 0;//内存池起始位置
    
    template <bool threads, int inst>
    char *__default_alloc_template<threads, inst>::end_free = 0;//内存池结束位置
    
    template <bool threads, int inst>
    size_t __default_alloc_template<threads, inst>::heap_size = 0;
    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, };

    空间配置函数allocate()

    具体实现如下:

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

    代码如下:

     /* n must be > 0      */
      static void * allocate(size_t n)
      {
        obj * __VOLATILE * my_free_list;
        obj * __RESTRICT result;
    
        if (n > (size_t) __MAX_BYTES) {
            return(malloc_alloc::allocate(n));
        }
        my_free_list = free_list + FREELIST_INDEX(n);
        // 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;
        if (result == 0) {
            void *r = refill(ROUND_UP(n));
            return r;
        }
        *my_free_list = result -> free_list_link;
        return (result);
      };

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

    refill()-为free list填充空间

    当发现对应的free list没有可用的空闲区块时,就需要调用此函数重新填充空间。新的空间将取自于内存池(将经由chunk_alloc()完成)。缺省取得20个新节点(新区块),但万一内存池空间不足,获得的节点数(区块数)可能小于20,内存池的管理后面会讲到。

    /* Returns an object of size n, and optionally adds to size n free list.*/
    /* We assume that n is properly aligned.                                */
    /* We hold the allocation lock.                                         */
    template <bool threads, int inst>
    void* __default_alloc_template<threads, inst>::refill(size_t n)
    {
        int nobjs = 20;
        //调用chunk_alloc(),尝试取得nobjs个区块作为free list的新节点,注意参数nobjs是pass by reference
        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);
        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++) {//将各节点串接起来(注意,索引为0的返回给客端使用)
            current_obj = next_obj;
            next_obj = (obj *)((char *)next_obj + n);
            if (nobjs - 1 == i) {
                current_obj -> free_list_link = 0;
                break;
            } else {
                current_obj -> free_list_link = next_obj;
            }
          }
        return(result);
    }

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

    源代码如下:

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

    /* We allocate memory in large chunks in order to avoid fragmenting     */
    /* the malloc heap too much.                                            */
    /* We assume that size is properly aligned.                             */
    /* We hold the allocation lock.                                         */
    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) {
            result = start_free;
            start_free += total_bytes;
            return(result);
        } else if (bytes_left >= size) {
            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);//注意此处申请的空间的大小
            // Try to make use of the left-over piece.
            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.
                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));
        }
    }

    上述的chunk_alloc()函数以end_free-start_free来判断内存池的水量,如果水量充足,就直接调用20个区块返回给free list。如果水量不足以提供20个区块,但还足够供应一个以上的区块,就拔出这不足20个区块的空间出去。这时候其pass by reference的nobjs参数将被修改为实际能够供应的区块数。如果内存池连一个区块空间都无法供应,对客端显然无法交代,此时便需利用malloc()从heap中配置内存,为内存池注入活水以应付需求。新水量的大小为需求量的两倍,再加上一个随着配置次数增加而愈来愈大的附加量。

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

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

    以上便是整个第二级配置器的设计。

    回想一些那个提供配置器标准接口的simple_alloc:

    template<class T, class Alloc>
    class simple_alloc{
        ...
    };
    
    SGI容器通常以这种方式来使用配置器:
    template<class T,class Alloc=alloc> //缺省使用alloc配置器
    class vector{
    public:
        typedef T value_type;
        ...
    protected:
        //专属之空间配置器,每次配置一个元素大小
        typedef simple_alloc<value_type,Alloc> data_allocator;
        ...
    };
    

      其中第二个tmplate参数所使用的缺省参数alloc,可以是第一级配置器也可以是第二级配置器。不过,SGI STL已经把它设为第二级配置器。

    deallocate()-空间释放函数

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

    SGI STL源代码:

     /* p may not be 0 */
      static void deallocate(void *p, size_t n)
      {
        obj *q = (obj *)p;
        obj * __VOLATILE * my_free_list;
    
        if (n > (size_t) __MAX_BYTES) {
            malloc_alloc::deallocate(p, n);
            return;
        }
        my_free_list = free_list + FREELIST_INDEX(n);
        // 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
      }
  • 相关阅读:
    回流和重绘
    php 异常捕获的坑
    每周散记 20180806
    转: Linux mount/unmount命令
    python http 请求 响应 post表单提交
    每周散记 20180723
    优惠劵产品分析
    c++ 软件版本比较函数
    每周散记
    转: 系统问题排查思路
  • 原文地址:https://www.cnblogs.com/wuchanming/p/4161774.html
Copyright © 2020-2023  润新知