剖析STL空间适配器

为什么要有空间配置器

1、小块内存带来的内存碎片问题
单从分配的角度来看。由于频繁分配、释放小块内存容易在堆中造成外碎片(极端情况下就是堆中空闲的内存总量满足一个请求,但是这些空闲的块都不连续,导致任何一个单独的空闲的块都无法满足这个请求)。

2、小块内存频繁申请释放带来的性能问题。
关于性能这个问题要是再深究起来还是比较复杂的,下面我来简单的说明一下。
开辟空间的时候,分配器会去找一块空闲块给用户,找空闲块也是需要时间的,尤其是在外碎片比较多的情况下。如果分配器其找不到,就要考虑处理假碎片现象(释放的小块空间没有合并),这时候就要将这些已经释放的的空闲块进行合并,这也是需要时间的。

3、小块空间太多会造成空间的浪费
每一次malloc 函数开辟出一块堆空间 在返回的指针前几个字节保存了开辟空间的大小。 这样free()的时候才知道传进去的指针到底意味着的多大的空间。 所以多开辟的空间叫配置空间。小空间向系统申请过多,这些记录空间大小的内存就太多造成内存浪费。
这里写图片描述

4、malloc new 出来的空间要 free delete 释放 如果忘记不释放会发生内存泄漏。

5、SGI STL同时解决了通用性和部分情况下不加锁解决线程安全的问题。

为了解决上面这些问题,所以就提出有了内存池的概念。内存池最基本的思想就是一次向heap申请一块很大的内存(内存池),如果申请小块内存的话就直接到内存池中去要。这样的话,就能够有效的解决上面所提到的问题。

在C++中的new 内含两个阶段的操作 (1) 调用operator new()配置内存,(2)调用对象构造函数构造对象内容 。delete (1)调用对象析构函数将对象析构 (2) 调用operator delete释放内存。STL将C++ 中new 与delete 关键字的功能做了细化分工, alloc::allocate()负责为对象配置内存 alloc::deallocate()负责释放内存。定义在#include< stl_alloc.h>文件中。 ::construct()负责在已分配好的内存上构造对象 ::destory()负责析构对象,定义在< stl_construct.h>中。STL的空间配置器只负责为对象配置空间不负责构造对象。

::construct()函数内部调用了 placement new 在制定指针指向的内存空间上初始化对象 调用对象构造函数。
::destroy()函数内部调用了对象的析构函数。

容器的构造函数往往会

分一级与二级

STL里面的空间配置主要分为两级,一级空间配置器(__malloc_alloc_template)和二级空间配置器(__default_alloc_template)。在STL中默认如果要分配的内存大于128个字节的话就是大块内存,调用一级空间配置器直接向系统申请,如果小于等于128个字节的话则认为是小内存,则就去内存池中申请。一级空间配置器很简单,直接封装了malloc和free处理,增加了_malloc_alloc_oom_handle(函数指针默认是0)作为用户自定义的你存不足处理机制用以试图释放内存资源。二级空间配置器是STL空间配置器的精华,二级空间配置器主要由memoryPool+freelist构成。

在STL的容器中 都有一个模板参数Alloc 代表空间配置器。
当代码定义了_USE_MALLOC这个宏时 Alloc是一级空间配置器
SGI STL没有定义这个宏,所以默认的Alloc为为二级空间配置器
进入默认的二级空间适配器代码后 如果发现需要配置的内存大于128字节
则调用以及空间配置器配置空间。

一级二级空间配置器都使用的是static函数和static变量 每一个进程的所有容器公用一份空间配置器。

一级空间配置器

模拟代码:

//一级空间配置器
template<int inst>
class _Malloc_Alloc_Template
{
    //Oom // Out_of_memory;
private:
    static void *Oom_Malloc(size_t);
    static void *Oom_Realloc(void* p, size_t size);
    static void(*__Malloc_Alloc_Oom_Handler)();//该函数的函数指针由用户自己知道自己设置。

public:
    static void* Allocate(size_t n)
    {
        void* result = malloc(n);//一级空间配置器直接使用malloc();
        if (NULL == result) result = Oom_Malloc(n);//malloc调用失败就调用out_of_memory时继续要malloc的处理函数
        return result;
    }

    static void* Reallocate(void* p, size_t new_size)
    {
        void* result = realloc(p, new_size);//一级空间配置器的reallocate()直接调用relloc()
        if (NULL == result) result = Oom_Realloc(p, new_size);//同Allocate。
        return result;
    }

    static void DeAllocate(void* p)
    {
        free(p);
    }

    //用户设置自己的 set_out_of_memory handler 将自己的内存不足处理方法传给 空间配置器
    //并保留原来的内存不足处理方法保留在old函数指针中。
    static void(*Set_Malloc_Handler(void(*f)()))()
    {
        void(*old)() = __Malloc_Alloc_Oom_Handler;
        __Malloc_Alloc_Oom_Handler = f;
        return (old);
    }

};

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;//循环调用用户自定义out of memory 处理例程 
        //希望释放后能成功分配内存
        if (NULL == My_Malloc_Handler){ _THROW_BAD_ALLOC };//用户没有定义则抛异常,丢出bad_alloc异常信息 或利用exit(1)终止程序。
        (*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 (NULL == My_Malloc_Handler) { _THROW_BAD_ALLOC };
        (*My_Malloc_Handler)();
        result = realloc(p, n);
        if (result) return result;
    }

}
typedef _Malloc_Alloc_Template<0> malloc_alloc;

一级空间配置器执行逻辑图:

这里写图片描述

SGI STL仿造C++标准库中的set_new_handler 搞了一个set_malloc_handler() 因为C++未提供相应于realloc()的内存配置操作

二级空间配置器

二级空间配置器是STL空间配置器的关键。
他解决了直接向系统频繁申请大量小块内存资源的诸多问题。

二级空间配置器使用内存池+自由链表的形式避免了小块内存带来的碎片化,提高了分配的效率,提高了利用率。SGI的做法是先判断要开辟的大小是不是大于128,如果大于128则就认为是一块大块内存,调用一级空间配置器直接分配。否则的话就通过内存池来分配,假设要分配8个字节大小的空间,那么他就会去内存池中分配多个8个字节大小的内存块,将多余的挂在自由链表上,下一次再需要8个字节时就去自由链表上取就可以了,如果回收这8个字节的话,直接将它挂在自由链表上就可以了。

为了便于管理,二级空间配置器在分配的时候都是以8的倍数对齐。也就是说二级配置器会将任何小块内存的需求上调到8的倍数处(例如:要7个字节,会给你分配8个字节。要9个字节,会给你16个字节),尽管这样做有内碎片的问题,但是对于我们管理来说却简单了不少。因为这样的话只要维护16个free_list就可以了,free_list这16个结点分别管理大小为8,16,24,32,40,48,56,64,72,80,88,86,96,104,112,120,128字节大小的内存块就行了。

这里写图片描述

为了将自由链表下面的结点串起来,又不引入额外的指针,所以我们要学会一物两用,因为在自由链表下面挂的最小的内存块都是8个字节,足够存放一个地址,所以我们就在这些内存块里面存放其他内存块的地址,这样的话就将这些内存块链接起来了。
这里写图片描述

二级空间配置器的逻辑步骤:

假如现在申请n个字节:
1、判断n是否大于128,如果大于128则直接调用一级空间配置器。如果不大于,则将n上调至8的倍数处,然后再去自由链表中相应的结点下面找,如果该结点下面挂有未使用的内存,则摘下来直接返回这块空间的地址。否则的话我们就要调用refill(size_t n)函数去内存池中申请。

2、向内存池申请的时候可以多申请几个,STL默认一次申请nobjs=20个,将多余的挂在自由链表上(尾插),这样能够提高效率。
进入refill函数后,先调chunk_alloc(size_t n,size_t& nobjs)函数去内存池中申请,如果申请成功的话,再回到refill函数。
这时候就有两种情况,如果nobjs=1的话则表示内存池只够分配一个,这时候只需要返回这个地址就可以了。否则就表示nobjs大于1,则将多余的内存块挂到自由链表上。
如果chunk_alloc失败的话,在他内部有处理机制。

3、进入chunk_alloc(size_t n,size_t& nobjs )向内存池申请空间的话有三种情况:
3.1、内存池剩余的空间足够nobjs*n这么大的空间,则直接分配好返回就可以了。
3.2、内存池剩余的空间leftAlloc的范围是n<=leftAlloc

//二级空间配置器
enum { _ALIGN = 8 };//小块内存最小字节数 每一块自由链表的内存都是_ALIGN的倍数。
enum { _MAX_BYTES = 128 };//定义的小型内存区块的上限
enum { _NUMBEROFLISTS = _MAX_BYTES / _ALIGN };//维护的自由链表的数量

template<bool thread, int init>
class _Default_Alloc_Template
{
private:
    static size_t ROUND_UP(size_t bytes)
    {//将需要的bytes 上调至八的倍数
        return (((bytes)+_ALIGN - 1) &~(_ALIGN - 1));//这样的方法效率更高
    }

    union obj{//自由链表结点 类型是union 为了不为维护链表所必须的指针而额外占用内存空间
        obj* FreeListLink;//指向另一个obj结构
        char client_data[1];//指向实际内存区块
    };

    static obj* volatile FreeList[_NUMBEROFLISTS];//用指针数组保存管理二级空间配置器的各个节点大小不同的自由链表
    static size_t FreeListIndex(size_t bytes)//通过开辟空间字节数找到其对应使用的自由链表在指针数组中的索引
    {
        return (((bytes)+_ALIGN - 1) / _ALIGN - 1);//这种方式效率高
    }

    static void * Refill(size_t n);//
    static char* Chunk_Alloc(size_t size, int& nobjs);//
    static char* start_free;//表示内存池的起始位置
    static char* end_free;//表示内存池的终止位置
    static size_t heapsize;//记录空间配置器共向系统堆申请了多大的内存。

public:

    static void* Allocate(size_t n)
    {
        obj * volatile * my_free_list;//操作自由链表中节点指针的指针
        obj* result;//定义指向返回内存块的指针
        if ((size_t)_MAX_BYTES < n){//申请大小超过128字节 调用一级空间配置器。
            return (malloc_alloc::Allocate(n));
        }
        my_free_list = FreeList + FreeListIndex(n);//自由链表中找到需要的内存块
        result = *my_free_list;//返回
        if (result == NULL){
            void* r = Refill(ROUND_UP(n));//如果这个自由链表没有内存块了 则从内存池上取20块这条链表结点指向空间大小的内存链接到指针数组的这个元素后面
            return r;
        }
        *my_free_list = result->FreeListLink;//指向下一个内存块 相当于头删
        return result;
    }

    static void DeAllocate(void* p, size_t n)
    {
        obj * q = (obj*)p;//Q是要是要还会自由链表中的节点
        obj * volatile * my_free_list;
        if ((size_t)_MAX_BYTES < n){//释放大小超过128字节 调用一级空间配置器。
            malloc_alloc::DeAllocate(p, n);
            return;
        }
        my_free_list = FreeList + FreeListIndex(n);//自由链表中找到需要的内存块
        q->FreeListLink = *my_free_list;//将要释放的节点头插还回自由链表
        *my_free_list = q;
    }
    //本质上这里的申请和释放是将一块内存空间交给用户管理和还给空间配置器管理。

};

template<bool thread, int inst>
char* _Default_Alloc_Template<thread, inst>::start_free = 0;

template<bool thread, int inst>
char* _Default_Alloc_Template<thread, inst>::end_free = 0;

template<bool thread, int inst>
char* _Default_Alloc_Template<thread, inst>::heapsize = 0;

template<bool thread, int inst>
typename _Default_Alloc_Template<thread, inst>::obj *  volatile 
_Default_Alloc_Template<thread, inst>::FreeList[_NUMBEROFLISTS] =
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };

这是网上找到的二级空间配置器执行流图:、
这里写图片描述

template<bool thread, int inst>
void* _Default_Alloc_Template<thread, inst>::Refill(size_t n) //填充一条自由链表的算法
{//参数n是指需要重新填充的链表的节点指向的空间的大小
    int nobjs = 20;//默认忘一条空链表填充20个该链表节点
    int i = 1;
    obj* volatile * my_free_list;
    obj* cur_obj, next_obj;
    obj* result;

    char* chunk = Chunk_Alloc(n, nobjs);//注意 nobjs是传引用 调用结束后nobjs表示实际上从内存池切下多少块内存块
    if (1 == nobjs) return (chuck);
    my_free_list = FreeList + FreeListIndex(n);//找到需要填充的链表的位置

    result = (obj*)chunk;//从内存池中切适当大小的内存 这里把第一块返回
    *my_free_list = next_obj = (obj*)(chunk + n);//这里把第二块先挂到指针数组对应位置下  //注意这里的n在传参数时已经调整到8的倍数
    for (i = 1;; i++){//循环进行了nobjs - 2次
        cur_obj = next_obj;
        next_obj = (obj*)((chat*)next_obj + n);//这里为什么是尾差不是头插?
        if (nobjs - 1 == i){                   //因为第一次从内存池取下的空间在物理上是连续的 尾插方便用 以后用完还回自由链表的就不是了
            cur_obj->FreeLinkList = NULL;//这里没有添加节点
            break;
        }
        else{
            cur_obj->FreeLinkList = next_obj;//nobjs - 2是最后一次添加节点
        }
    }
    return result;
}

template<bool thread, int init>
char* _Default_Alloc_Template<thread, init>::Chunk_Alloc(size_t size, int& nobjs)
{
    char* result;
    size_t TotalBytes = size * nobjs;//需要从內存池切多少字节
    char* LeftBytes = end_free - start_free;//内存池剩下多少字节
    if (TotalBytes <= LeftBytes){//内存池剩余空间够20块n个字节大小
        result = start_free;
        start_free += TotalBytes;
        return result;
    }
    else if (TotalBytes > LeftBytes){//内存池剩余空间不足
        nobjs = LeftBytes / size;//减少切下的内存块数
        TotalBytes = nobjs * size;//重新计算共从内存池切下多少字节
        result = start_free;
        start_free += TotalBytes;
        return result;
    }
    else{
        if (LeftBytes > 0){//内存池中还有不到1块内存  将剩的这不到一块内存挂到自由链表下
            obj* volatile * my_free_list = FreeList + FreeListIndex(LeftBytes);//LeftBytes 一定是8的倍数 因为之前是按照八的倍数切得内存
            (obj*)(start_free)->FreeListLink = *my_free_list;//头插
            *my_free_list = (obj*)start_free;
        }
        size_t BytesToGet = 2 * TotalBytes + ROUND_UP(heapsize >> 4);//这里通过heapsize进行负反馈调节如果之前申请过的堆空间越大 这里就多开辟些内存池
        start_free = (char*)malloc(BytesToGet);

        if (0 == start_free){//heap 空间不足,malloc()失败。
            obj* volatile *my_free_list;//在比当前需求的自由链表保存内存块大小大的自由链表中寻找一块空间作为内存池 并切下作为小自由链表的内存块
            obj* p;
            for (int i = size; i <= _MAX_BYTES; i += _ALIGN){
                my_free_list = FreeList + FreeListIndex(i);
                p = *my_free_list;
                if (0 != p){
                    *my_free_list = p->FreeListLink;
                    start_free = (char*)p;
                    end_free = start_free + i;
                    return(Chunk_Alloc(size, nobjs));
                }
            }
            end_free = 0;
            start_free = (char*)_Malloc_Alloc_Template::Allocate(BytesToGet);
            //分配失败时start_free是0 这时如果end_free不是0的话 下次end_free - start_free
            //会得到很大一块非法空间。
        }
        heapsize += BytesToGet;//记录向系统堆空间申请大小。
        end_free = start_free + BytesToGet;//标记好新的内存池的大小。
        return (Chunk_Alloc(size, nobjs));
    }
}
空间配置器的缺陷:

1、在空间配置器中所有的函数和变量都是静态的,所以他们在进程结束的时候才会被释放发。二级空间配置器中没有将申请的内存还给操作系统,只是将他们挂在自由链表上。所以说只有当你的进程结束了之后才会将开辟的内存还给操作系统。

2、由于它没有将内存还给操作系统,所以就会出现二种极端的情况。
2.1假如我不断的开辟小块内存,最后将整个heap上的内存都挂在了自由链表上,但是都没有用这些空间,再想要开辟一个大块内存的话会开辟失败。
2.2二级空间配置器会造成内碎片问题,这是因为二级空间配置器会对每一次要申请的内存进行ROUND_UP()向上调整到八的倍数。假设二级空间配置器中的极端的情况下一直申请char,则就会浪费7/8的空间。但是整体来说,空间配置器的性能还是蛮高的。

容器如何对空间适配器使用

STL中的容器是使用迭代器对其操作的,所以要封装alloc::allocate()
使它可以配合迭代器。所以有了simplate_alloc

template<class T, class Alloc>
class simple_alloc{
    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));
    }
};

template<class T, class Alloc = alloc>
class Vector{
protected:
    typedef simple_alloc<ValueType, Alloc> data_allocator;

    void deallocate(){
        if (...)
            data_allocator::deallocate(start, end_of`
storage - start);
    }
};

实际上容器的构造函数和新添加的元素涉及到要开辟空间时。总是要先调用空间配置器开辟空间 之后调用construct()构造对象 或者使用迭代器(实际上是使用指针)填充开辟好的空间。
实际上的SGI STL 为了实现效率进行了迭代器萃取和针对不同对象类型使用不同填充开辟好空间的方法,又对 construct()和fill_n()等函数做了封装。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值