庖丁解牛:剖析STL库空间配置器

在学习一个技术之前,知道为什么要学习它,学习它有什么用是必要的,所以我要先说明为什么需要空间配置器:

内存碎片问题
这里写图片描述
假设依次向系统申请了16字节,8字节,16字节,4字节的空间,还剩下面的8字节空间未被分配;
随后,这两个16字节的空间被收回;
(淡紫色部分表示已经被系统收回,粉色部分表示已申请,白色部分表示未被分配)
这时一共有16+16+8(淡紫色+白色)= 40字节空间,我要申请一个24字节的空间;
看似没问题,但是却申请不了连续24字节的空间,这就是内存碎片的外碎片问题;
至于内碎片,下文会详谈。

内存碎片的危害:频繁申请小块内存,效率低下。
所以说空间配置器解决外碎片问题是必要的!

STL空间配置器分为一级和二级,我会逐一详细介绍,以下的代码是我自己通过剖析源码,造轮子简单实现的:
一:STL一级空间配置器
一级空间配置器在封装了malloc和free的基础上,模拟了C++的set_new_handler()机制来处理系统内存不足的问题;
具体情况我通过代码+分析的方式来呈现:

typedef void(*HANDLER_FUNC) ();

template <class inst>
class __Malloc_Alloc_Template
{
    static HANDLER_FUNC __malloc_alloc_oom_handler;//static成员在类外初始化,默认初始化为NULL
public:
    static void* Allocate(size_t n)
    {
        void* result = malloc(n);//申请n字节的空间,一级空间配置器先调用malloc
        if (result == NULL)//如果申请失败,那么就会模拟C++set_new_handler()机制来处理
            result = OOM_Malloc(n);
        return result;
    }

    static void Deallocate(void* p/*, size_t n*/)
    {
        free(p);
    }

    //该函数是重新写一个处理问题的方法,并记录下旧方法
    static void(*HANDLER_FUNC(void(*f)()))()
    {
        void(*old)() = __malloc_alloc_oom_handler;
        __malloc_alloc_oom_handler = f;
        return (old);
    }

private:
    static void* OOM_Malloc(size_t n)
    {
        while (1)
        {
            //__malloc_alloc_oom_handler默认初始化为空,处理方法为空时抛异常
            if (__malloc_alloc_oom_handler == NULL)
                throw bad_alloc();

            //不为空的话,方法应该处理好问题,不然会一直循环
            __malloc_alloc_oom_handler();
            //假设新方法解决了问题,malloc申请n字节成功,则返回,否则一直循环直到解决问题!
            void* ret = malloc(n);
            if (ret)
                return ret;
        }
    }
};

//初始化
template<class inst>
HANDLER_FUNC __Malloc_Alloc_Template<inst>::__malloc_alloc_oom_handler = NULL;

这就是一级空间配置器的原理,比较简单,接下来是测试:
这里写图片描述
此时没有重写处理方法,它默认为空会抛异常:
这里写图片描述
然后重写方法,就会根据重写的方法来解决问题,这里简单测试,方法里直接退出。
这里写图片描述
这里写图片描述

二:STL二级空间配置器
一级空间配置器只是开胃小菜,真正的大餐在二级空间配置器这里!

二级空间配置器的原理,简单来说:
1.当申请的内存大于128字节时,会直接调用一级空间配置器(因为申请的内存够大,不会产生外碎片);
2.引入了自由链表和内存池的机制:
这里写图片描述
自由链表的长度为16,每一个位置都挂有对应大小的内存块(比如第1,4,10个下分别挂有16字节,40字节,88字节的内存块),最大挂128字节的内存块(第15个);
当自由链表没有内存块时,则向内存池申请内存而不是直接向系统要,这样效率会高很多;
_startFree和_endFree指向内存池的开始和结束;
当内存池的空间不够时,才向系统申请,而且一次会申请很大块的内存,有效避免了频繁申请小块内存的问题。

当然,以上只是简单说明,具体很多细节,还是要通过代码体现:
二级空间配置器的成员数据

private:
    //内存池
    static char* _startFree;//内存池开始的位置
    static char* _endFree;//内存池结束的位置
    static size_t _heapsize;//记录每次向系统申请内存池的大小的累加和。 

    //链接下一个
    union Obj
    {
        Obj* _freeList_link;//指向下一个的指针
        char client_data[1];
    };

    enum { _ALICN = 8 };//排列间隔
    enum { _MAX_BYTES = 128 };//最大值,超过128调用一级空间配置器
    enum { _NFREELISTS = _MAX_BYTES / _ALICN };//链的长度,为16

    //自由链表,长度为16
    static Obj* _freeList[_NFREELISTS];//指针数组

对于数据,我重点要说明两点:
1.union Obj
这是一个联合体,自由链表就是该联合体类型的,这样写的目的是,不管你挂多大的内存,我只需强转为Obj*类型,用一个_freeList_link指针来连接。
2.排列间隔为8:
排列间隔太大的话不好,会浪费空间,同样申请8字节空间,排列间隔如果为8,则刚好分配一个8字节的内存块;若排列间隔变大为16。那么最小只能分配一个16字节的空间,浪费了8字节的空间,这就是内碎片!
所以说空间配置器虽然解决了外碎片的问题,但还是存在内碎片的问题。
那么排列间隔如果设的很小呢,我可以这么说:8字节已经是最小了,因为在64位下,指针的大小是8字节,那么连Obj的一个指针都存不下了。

二级空间配置器初始化:

template <class inst, bool threads = true>
char* __Default_Alloc_Template<inst, threads>::_startFree = 0;

template <class inst, bool threads = true>
char* __Default_Alloc_Template<inst, threads>::_endFree = 0;

template <class inst, bool threads = true>
size_t __Default_Alloc_Template<inst, threads>::_heapsize = 0;

template <class inst, bool threads = true>
typename __Default_Alloc_Template<inst, threads>::Obj*
__Default_Alloc_Template<inst, threads>::_freeList[_NFREELISTS] =
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };

因为都是static成员,所以需要类外初始化。

扫描二维码关注公众号,回复: 2231698 查看本文章

二级空间配置器方法:

public:
    static void* Allocate(size_t n)
    {
        cout << "二级空间适配器申请" << n << "字节空间" << endl;
        //如果申请的大于128字节,用一级
        if (n > _MAX_BYTES)
            return __Malloc_Alloc_Template<int>::Allocate(n);

        size_t index = FREELIST_INDEX(n);//在自由链表的位置

        //当该位置有空间时,直接使用(头删),拿内存块返回
        if (_freeList[index] != NULL)
        {
            cout << "自由链表第" << index << "处挂有内存,头删取一块" << ROUND_UP(n) << "大小的空间" << endl;
            Obj* ret = _freeList[index];
            _freeList[index] = ret->_freeList_link;
            return ret;
        }
        else//该位置没有挂空间,那么就需要去内存池里找
        {
            cout << index << "处没有挂内存,需要从内存池里找" << endl;
            //需要申请8的整数倍的空间 ROUND_UP(n)
            return Refill(ROUND_UP(n));
        }
    }

    static void Deallocate(void* p, size_t n)
    {
        //大于128.call一级释放
        if (n > _MAX_BYTES)
        {
            __Malloc_Alloc_Template<int>::Deallocate(p);
            return;
        }

        Obj* obj = (Obj*)p;//先把空间转为Obj*,再头插到自由链表的对应位置
        size_t index = FREELIST_INDEX(n);
        obj->_freeList_link = _freeList[index];
        _freeList[index] = obj;
    }

private:
    static size_t FREELIST_INDEX(size_t n)
    {
        //当不是8的整数倍时,需要多给一些,比如10字节,就需要给16字节的空间
        //那么会有6字节色空间没有用,这叫做叫内碎片

        //if (n % 8 == 0)
        //  return ((n / 8) - 1);
        //return n / 8;

        //上面的办法虽然容易想到,但是下面的方法会更高效
        return (((n + _ALICN - 1) / _ALICN) - 1);
    }

    static size_t ROUND_UP(size_t n)
    {
        //申请8的整数倍的空间
        //          (n+7)&(~7)
        return ((n + _ALICN - 1) & (~(_ALICN - 1)));
    }

    static void* Refill(size_t bytes)
    {
        //比如需要开32字节的空间(butes为32):
        //走到这里,说明自由链表挂32的位置为空,所以处理方法是区内存池里找

        //具体机制:
        //如果空间足够,就直接挂20个32字节的空间,以后再申请32字节的空间,就可以直接在自由链表拿内存块,提高效率
        //如果空间不够,但至少得需要够1个32字节的空间

        size_t nobjs = 20;//如果空间够,直接挂20块
        char* chunk = ChunkAlloc(bytes, nobjs);

        if (nobjs == 1)//如果只剩一个bytes的空间,直接返回该空间
        {
            cout << "系统只剩一块空间" << endl;
            return chunk;
        }

        //如果是多个空间,需要返回一块,把剩下(nobjs-1)挂到相应的位置
        //先挂起一块
        size_t index = FREELIST_INDEX(bytes);
        cout << "从系统中获取" << nobjs << "个" << bytes << "大小的内存块,返回一块,挂起19块到" << index << "处"<<endl;;
        Obj* cur = (Obj*)(chunk + bytes);//chunk位置需要返回
        _freeList[index] = cur;

        for (size_t i = 1; i < nobjs - 1; i++)//只需要挂nobjs-2块
        {
            Obj* next = (Obj*)(char*)cur + bytes;
            cur->_freeList_link = next;
            cur = next;
        }
        //cur->_freeList_link = NULL;
        return chunk;
    }

    static char* ChunkAlloc(size_t bytes, size_t& nobjs)
    {
        size_t leftBytes = _endFree - _startFree;//内存池剩余的内存
        size_t totalBytes = bytes*nobjs;//一共挂20块需要多大的空间

        cout << "内存池剩余" << leftBytes << "字节的空间,一共需要" << totalBytes << "字节的空间。" << endl;

        if (leftBytes > totalBytes)//当内存池够用
        {
            char* chunk = _startFree;
            _startFree += totalBytes; //_startFree往后移,内存池变小
            return chunk;//所以这段需要的空间就是从chunk到_startFree这段
        }

        else if (leftBytes >= bytes)//不够20个bytes,但至少得够一个
        {
            nobjs = leftBytes / bytes;//得重新计算nobjs(剩下的内存够几个bytes)
            totalBytes = nobjs*bytes;//同样,总共需要的空间也会变,也需要重新计算

            char* chunk = _startFree;//挂了nobjs块,内存池往后移,返回需要的空间
            _startFree += totalBytes;
            return chunk;
        }

        else//当内存池已经连一个bytes的空间都没有了的机制
        {
            //1.虽然内存池没有bytes的空间,但是内存池还可能有一些空间
            //比如,bytes为64,但只剩下32的空间了,所以一个bytes的空间也没有
            //但是,还是需要处理这剩下的空间:在对应位置挂起剩余的空间
            if (leftBytes > 0)//还有剩余的空间(小于bytes)
            {
                size_t index = FREELIST_INDEX(leftBytes);//剩余空间挂起来,对应自由链表的位置
                cout << "内存池的空间大于0小于" << bytes << "字节,将剩余的" \
                    << totalBytes << "字节挂到" << index << "位置" << endl;

                ((Obj*)_startFree)->_freeList_link = _freeList[index];
                _freeList[index] = (Obj*)_startFree;
            }

            //2.找系统申请空间
            size_t bytesToGet = totalBytes * 2 + (ROUND_UP(_heapsize >> 4));//需要申请的大小,一次申请比较大的一块内存

            _startFree = (char*)malloc(bytesToGet);
            if (_startFree == NULL)//如果连系统都没有那么多空间了,还是不能放弃
            {
                cout << "向系统申请" << bytesToGet << "字节空间失败" << endl;
                //还是不放弃,到更大的自由链表中找:具体过程再总结一遍
                /*比如说要申请32字节空间,32字节对应的自由链表没有挂内存,去内存池找,内存池
                也没有这么多空间,再malloc向系统申请,系统也没有32字节的空间了,这时候可以
                在自由链表更大的位置看看有没有挂内存块,比如如果64字节位置的自由链表挂有内
                那么可以用。*/
                for (size_t i = FREELIST_INDEX(bytes); i < _NFREELISTS; i++)//从bytes对应的位置找,直到自由链表的最后位置
                {
                    if (_freeList[i])//如果更大的位置挂有内存块
                    {
                        _startFree = (char*)_freeList[i];
                        _freeList[i] = ((Obj*)_startFree)->_freeList_link;
                        _endFree = _startFree + (i + 1)*_ALICN;
                        //此时,把对应内存块拿给内存池
                        return ChunkAlloc(bytes, nobjs);
                    }
                }
                //没办法了,系统没空间了,更大的自由链表没有挂内存,调一级,其实到这里已经可以放弃治疗了
                _endFree = 0;
                _startFree = (char*)__Malloc_Alloc_Template<int>::Allocate(bytesToGet);
            }

            cout << "向系统申请" << bytesToGet << "字节成功,再次调用ChunkAlloc()" << endl;

            _heapsize += bytesToGet;
            _endFree = _startFree + bytesToGet;
            return ChunkAlloc(bytes, nobjs);
        }
    }

猜你喜欢

转载自blog.csdn.net/han8040laixin/article/details/81077712