简述STL-->空间配置器

空间的配置与释放

原来思想中存在的问题:

在我们没有了解到STL中的空间配置器的时候,我们所了解的资源的申请无非就是malloc/freenew/delete还有new[]/delete[]组合,它们用起来很简单方便,但是我们都知道,它们申请失败以后,直接就是退出程序不做其他处理;那么,真的是没有内存让我们用了吗?其实并不然,下面我们就来列出我们曾经用的方法中的几大缺点:

  • 效率低下:(我们每次申请空间都是要在内存中宏去申请,我们设想一次申请多个存放起来,这样下次遇到这样大小的直接就可以用了,效率提高很多啊!!)

  • 内存泄漏:(如果我们编写的代码很长,并且有多次使用malloc/free、new/delete、new[]/delete[],这样的空间申请组合,我们很难分辨清楚到底是哪个对应哪个,这样很容易造成空间泄漏)

  • 内存碎片:(我们在申请空间的时候,可能出现有两个空间中间预留了一点空间,但是这个空间确不能满足我们的需求,这样这段空间就会一直这样存放在这里,不能使用造成内存碎片)

  • 额外的开销:(我们都知道,在申请一段空间的时候,我们并不是仅仅申请了这么多的空间,其中还多申请了好多空间来维持这段我们想要的空间,这样很多空间都是为了修饰别的空间而占用了)

  • 申请失败结束:(其实,我们前面的处理都是这样的,但是在空间配置器中就不是这样来处理的了)

一级空间配置器(所申请的字节数>128):

简述STL-->空间配置器_第1张图片

    static void* allocate(size_t n)
    {
        void *result = malloc(n);       //一级空间配置器直接调用malloc
        if (0 == result)
            result = oom_malloc(n);     //在申请失败后调用特殊函数进行处理;(解决了原来申请失败直接返回的问题)
        return result;
    }

我们从图中可以看到,一级空间配置器其实就是重新封装了malloc(),free(),但是,在这个基础上还添加了一个对于申请失败的处理函数:

    static void* 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;
        }
    }

上面这段函数就是,我们对申请失败的处理,其原理就是:我们当前所申请的空间没有了,但是可能会有原来申请的空间现在也不用,闲着也是闲着,把你释放掉借给我就可以了,这样死循环下去,直到我们所申请的空间得到为止,满足用户要求;

总结一下

一级空间配置器中解决了我们申请失败后不用直接返回的问题,可以继续申请直到用户得到空间为止;其实,一级空间配置器是很简单的,不需要我们来花费很大的精力;

第二级空间配置器(所申请的空间<128个字节):

在二级空间配置器中,是以内存池来管理这些空间,每次配置一大块内存,并维护对应的自由链表,下次若再有相同大小的内存需求,就直接从自由链表中去取(解决了我们不用每次都去内存中取,造成的效率低下的问题);为了方便管理,SGI第二级空间配置器会主动将任何小额区块的内存需求上调至8的倍数(解决内存碎片的问题,但是又造成了内存池内部内存碎片(这是STL中的一大缺点));
简述STL-->空间配置器_第2张图片
在上图中,我们可以看到STL中内存池与free_list之间的一个图形,我们可以看到,在free_list中挂接的节点其实就是从内存池中切割下来挂接上去的,所以我们要研究一个这个节点的相关知识:

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

我们大家都知道,在维护链表的时候,每个节点需要额外的指针,这样就造成了一种额外的负担,但是,union就很机智的避免了这一点,可以指向另一个obj也可以指向实际区块,还不用浪费空间,机智,机智;

上面我们说过,为了方便管理二级空进啊配置器会将任何小额区块的内存需求上调至8的倍数:

    static size_t ROUND_UP(size_t bytes)                //将bytes提升至8的倍数
    {
        return (((bytes)+_ALIGN - 1) & ~(_ALIGN - 1));
    }

还有一个在free_list中我们寻找内存块对应的my_free_list时的定位函数:

    static size_t FREELIST_INDEX(size_t bytes)          //根据区块的大小,决定使用第n号free_list。n从0开始
    {
        return (((bytes)+_ALIGN - 1) / _ALIGN - 1);
    }

下面我们来仔细研究一个二级空间配置器的allocate函数:

    static void* allocate(size_t n)
    {
        obj * volatile * my_free_list;
        obj * result;

        if (n > 128)
            return (_Malloc_alloc_<0>::allocate(n));
        my_free_list = free_list + FREELIST_INDEX(n);
        result = *my_free_list;
        if (result == 0)
        {
            void * r = refill(ROUND_UP(n));//由于free_list中没有数据,就由free_list向内存池中发出请求
            return r;
        }
        *my_free_list = result->free_list_link;
        return (result);
    }

在函数中,我们可以看到当字节数大于128时,我们就直接交给一级空间配置器,然后进行去自由链表中找(我们前面说过,直接去内存池中找的话,每次都需要耗费时间),但是自由链表中没有的话,就交给refill()函数进行处理,处理完成后直接返回给用户就可以了;

refill(size_t n)

对于refill()以及所包含的函数,这就是二级空间配置器的核心所在:

void* _Default_alloc_::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)                                 //只有一块的话,直接返回给用户,不需要再free_list中插入节点
        return chunk;
    my_free_list = free_list + FREELIST_INDEX(n);   //调整free_list准备插入新节点
    result = (obj*)chunk;                           //保存一块返回给用户
    *my_free_list = next_obj = (obj*)(chunk + n);   //把取自内存池中的元素指向新分配的空间
    for (i = 1;; i++)                               //从1开始,0返回给客户
    {
        current_obj = next_obj;
        next_obj = (obj*)((char*)next_obj + n);     //每次next_obj的调整是根据n的偏移量来调整的
        if (nobjs - 1 == i)
        {
            current_obj->free_list_link = 0;
            break;
        }
        else
        {
            current_obj->free_list_link = next_obj;
        }
    }
    return result;
}

在refill()函数中,我们可以看到默认向内存池中申请的是20个,但是如果内存池中只有1个就直接返回给用户,如果申请的挺多的,那么就把多申请的挂接到free_list中就可以了;

chunk_alloc(size_t size, int& nobjs)

char* _Default_alloc_::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)                    // 1< 内存池大小
    {
        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);
        if (bytes_left > 0)                         //可能造成的小内存无法满足size,要将它插入到free_list合适的位置,不然可能造成内存泄漏
        {
            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)                        //Heap中也没有空间了,这就很尴尬了;
        {
            int i;
            obj  * volatile * my_free_list, *p;
            for (i = size; i <= _MAX_BYTES; i += _ALIGN)   //在大于size的free_list中找是否存在申请了但没有用的空间,从size开始可以避免多线程存在的安全隐患
            {
                my_free_list = free_list + FREELIST_INDEX(i);
                p = *my_free_list;
                if (0 != p)                                 //free_list中有尚未使用的空间
                {
                    *my_free_list = p->free_list_link;      //调整free_list释放出未用的区块
                    start_free = (char*)p;
                    end_free = start_free + i;
                    return (chunk_alloc(size, nobjs));      //递归调用自己,为了修改nobjs;
                    //注意,任何剩余的不能满足需求的剩余空间存于合适的free_list,避免内存泄漏
                }
            }
            end_free = 0;   //此处可能出现一级空间配置器出现的抛异常情况,可能出现start_free为空,但end_free不为空,所形成的空间不可知,在使用的时候就奔溃
            start_free = (char *)_Malloc_alloc_::allocate(bytes_to_get);//调用一级空间配置器,已用my_malloc_handler尝试找空间
        }
        heap_size += bytes_to_get;
        end_free = start_free + bytes_to_get;
        return (chunk_alloc(size, nobjs));          //递归调用自己,修正 nobjs
    }
}

对于上面想内存池申请的函数,我们大致分为三层:
1. 我们申请20个内存池中完全能够提供给我们,我们直接返回这段地址的起始位置;
2. 内存池能提供给我们的不能满足20个但是可以提供至少1个所以,我们把能提供的全部占有了,返回起始位置;
3. 内存池中连一个都提供不了,这样我们就得四处去找空间了,又分为2步:

  • 当内存池不能满足我们一个空间的要求,但是它有可能还有空间剩下了,所以我们必须得把这些空间处理掉,不然内存泄漏。。。。所以我们根据剩余空间的大小,直接把他挂接到free_list就可以了;

  • 处理完剩余空间,我们就要像堆中申请空间了,如果堆中空间足够那么,直接给我们就可以了,但是堆中空间不足的话,我们就需要在比自己这段空间大的free_list中寻找他们申请了但是并没有使用的空间,借我试试,如果找到以后,就返回给用户,但是必须要做的就是,对于切割剩余的空间我们一定要妥善处理,避免内存泄漏;

deallocate(void* p, size_t size)

    static void* deallocate(void* p, size_t size)
    {
        obj *q = (obj *)p;
        obj * volatile * my_free_list;
        if (n > (size_t _MAX_BYTES))                    //大于128直接交由一级空间配置器解决;
        {
            _Malloc_alloc_<0>::deallocate(p, n);
            return;
        }
        my_free_list = free_list + FREELIST_INDEX(n);   //找到p所对应的free_list
        q->free_list_link = *my_free_list;              //用头插法把空间返回给free_list
        *my_free_list = q;
    }

总结一下

对于二级空间配置器,我们最重要的是了解到了他对于小块内存处理时,对于free_list和内存池的应用,这样我们解决了前面我们在刚刚开始提到的几个问题;
简述STL-->空间配置器_第3张图片

从图中,我们可以看到,对于使用哪个空间配置器,是有一个宏来管理的,默认使用二级空间配置器;并且对于空间配置器的申请和释放封装了一个函数:

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(p));
    }
};

空间配置器中存在的几个问题:
- 为什么函数全部是static:对于非static函数,每个对象都需要一个空间配置器,但是静态的时候,一个对象只要一个就可以了;

  • size为什么是从size+8开始的:解决线程安全问题,不同的线程速度不同

  • _endfree为什么还要置空:在一级空间配置器中可能抛异常,造成start_free为空,这样这段空间不可知,运行必然崩溃

  • 空间释放问题:在一级空间配置器中直接释放,而二级空间配置器中,我们看到的是它直接归还给自由链表就可以了,并没有进行free;其实,他们是在程序运行结束的时候释放的;

  • 为什么是8字节开始: 在32和64位平台下能够正常使用;

空间的构造与析构

在STL中的构造函数中,是直接采用定位new表达式来进行对象的构建:

template <class T1, class T2>
inline void Construct(T1* p, const T2& value)
{
    new(p)T1(vlaue);        //定位new表达式
}

在析构的时候我们就要分为两种:

  • 内置类型不需要析构函数来析构

  • 自定义类型需要用户自己来析构

所以,我们在调用析构函数的时候需要辨别是否是内置类型,所以,我们必须用到的就是类型萃取:

#pragma once

struct TrueType
{};

struct FalseType
{};

template<class T>
struct TypeTraits
{
    typedef FalseType hasTrivialDefaultConstructor;
    typedef FalseType hasTrivialCopyConstructor;
    typedef FalseType hasTrivialAssignmentOperator;
    typedef FalseType hasTrivialDestructor;
    typedef FalseType isPODType;
};

template<>
struct TypeTraits<int>
{
    typedef TrueType hasTrivialDefaultConstructor;
    typedef TrueType hasTrivialCopyConstructor;
    typedef TrueType hasTrivialAssignmentOperator;
    typedef TrueType hasTrivialDestructor;
    typedef TrueType isPODType;
};

template<>
struct TypeTraits<char>
{
    typedef TrueType hasTrivialDefaultConstructor;
    typedef TrueType hasTrivialCopyConstructor;
    typedef TrueType hasTrivialAssignmentOperator;
    typedef TrueType hasTrivialDestructor;
    typedef TrueType isPODType;
};

对于上述所列的几个类型萃取的函数,并不能处理迭代器类型的变量:

template<class Iterator>
struct IteratorTraits
{
    typename typedef Iterator::ValueType ValueType;     //对象的类型
    typename typedef Iterator::DifferenceType DifferenceType;       //迭代器类型
    typename typedef Iterator::Reference Reference;
    typename typedef Iterator::Pointer Pointer;
    typename typedef Iterator::IteratorCategory IteratorCategory;
};


template<class T>
struct IteratorTraits
{
    typedef T ValueType;
    typedef T& Reference;
    typedef T* Pointer;
    typedef int DifferenceType;
    typedef RandomAccessIteratorTag IteratorCategory;
};


template<class T>
struct IteratorTraits<const T*>
{
    typedef T ValueType;
    typedef const T& Reference;
    typedef const T* Pointer;
    typedef int DifferenceType;
    typedef RandomAccessIteratorTag IteratorCategory;
};

对于迭代器类型的类型萃取,我们必须用一张图来解释:
简述STL-->空间配置器_第4张图片

  • 第一步:通过迭代器类型萃取将IteratorTraits中ValueType所对应的拿出来代替这个表达式

  • 第二步:将T返回补充类型萃取函数;

  • 第三步: 用TypeTraits来判断对应的类型萃取函数是否被特化,用函数看萃取结果;

  • 第四步:返回萃取结果;

所以当类型萃取结束后,判断类型结束,就可以进行节点的释放,一个完整的空间配置器才算结束;

全部代码上传GitHubSTL_alloc

你可能感兴趣的:(C/C++)