花开自有丶花落 发表于 2019-7-19 15:51:09

(原创)手撸一个简化版std::allocator内存池,附源码及解释

本帖最后由 花开自有丶花落 于 2019-8-26 23:47 编辑

直奔主题,对于本帖,代码中的大部分注释我已经删掉,取而代之的是再本帖中尽量说明
我将主要分为五大块来介绍,顺便为自己重新捋一下思路,代码中也有某些未完善的不足,仅供学习,转载请注明出处
另:第一次写这么长的东西,有些地方写的不好还请见谅
PS:后面还有点没写完,可能是因为有些长,写草稿有些显示不出来,先发出来看下
      发表后还是显示不出来,源码最下面,可以直接下载

[*]简介与使用
[*]一些宏与常量的说明
[*]各个类与函数的整体介绍
[*]每个函数内部的逻辑
[*]不足


一、简介
        主要解决的内存碎片问题,为了程序长期稳定运行,而付出代价(时间与空间)代码手动管理
        总体思路参照std::allocator,三个类以及一个联合体完成实现,另外已在一个近千行的多线程网络程序中测试(如要使用请自行决断)
        主要通过维护16个长度不同的自由链表,8字节,16字节,32字节等等,一直到128字节,每次申请通过找到最适合的长度,找到对应链表,由链表分配内存
        运行环境:vs2017
        使用,如下图,只需要简单的将其包含在main文件中的第一行,重载的new和delete即可替换掉默认的
       
二、一些宏与常量的说明
        .h的头文件,首先看到的就是一下几行:

       
[*]_DEBUG宏定义
        宏定义_DEBUG,这个在vs中调试模式下会有这个宏,release下就不会定义这个宏
        这个宏的主要作用:
                调式模式:xPrintf(...)替换为printf(__VA_ARGS__),输出调试信息
                release模式:把xPrintf(...)替换为空,不会有输出信息
[*]两个常量(PS:如果看不懂这一块内容,可以先看第三块,遇到了在回来找)
        const int MA_MAX_LENGT = 128;       
                这个128代表的是内存池最大管理的可分配长度,在本帖的内存池中,主要管理8,16,24,32,一直到MA_MAX_LENGT(128),等8的倍数的内存分配
                不足8的倍数则向上取整,如new char,需要13字节内存空间,则为其分配16字节空间,回收时也由内存池回收
                大于128的空间。就直接调用C的malloc()函数申请
                为什么选128,因为std::allocato是这么做的,我也没有很多经验,不确定选取哪个界限,以内存池管理会有好的行为
        const int SIZEOFSHORT = sizeof(short);
                这个常量一般都是2字节,只是用的比较多,不想在运行时多次计算,就放到这里。
[*]重载new与delete(PS:如果看不懂这一块内容,可以先看第三块,遇到了在回来找)
        只是在头文件中声明了重载new与delete,具体实现可在.cpp中找到,第三块和第四块内容也会说

三、各个类与函数的整体介绍
        本块是最重要的,主要自顶向下讲述三个与联合体间的交互,介绍顺序为class SHH_MA;        class free_lists;        class free_listor;        union obj;

[*]首先是各个类间的依赖关系(配合主要功能介绍食用第三小节)

[*]然后介绍一个各个类的主要作用
        class SHH_MA;
                SHH_MA是最上层的类,前缀是名字缩写喽,MA就是memory alloc(内存分配)的缩写了
                SHH_MA为operator new和operator delete服务,也就是说operator new和operator delete是通过SHH_MA来分配内存的
                operator new和operator delete直接通过SHH_MA分配和回收内存
        class free_lists;       
                SHH_MA会持有一个class free_lists*的数组
                free_lists是一个基类,class free_listor会继承它
                free_lists提供一组接口,SHH_MA通过持有的 free_lists*数组操作free_listor对象
        class free_listor;
                free_listor负责内存的实际分配和实际回收,是一个模板
        union obj;
                obj为free_listor提供数据结构支持,也是一个模板
[*]最后是各个成员函数及数据的主要功能(PS:建议与依赖关系图配合食用)
        new和delete的重载
        因为重载的new和delete是最外层的调用,所以先说一下这个
        如图中,仅是通过SHH_MA::SHH_MA_OBJ()得到单例对象,然后调用其分配和回收内存的函数,更多的细节留给下层的类去处理
        SHH_MA::SHH_MA_OBJ()见下
       
        class SHH_MA;
       
               
                       
                                数据:
                                如下图
                                一个free_lists*数组
                                还有16个派生自free_lists的ree_listor,这16个free_listor分别负责维护不同大小内存的分配
                                free_lists数组是一个映射数组,这个数组有MA_MAX_LENGT + 1,即129,索引为0到128,分别指向16个free_listor
                                举例,freedom_list_map,存储指向free_listor<8> md_mem8的指针,这样比如说我这时候要申请一个5字节的空间
                                直接用这个申请的长度5作为索引,freedom_list_map的位置指向md_mem8,通过md_mem8的指针直接调用其分配内存函数
                                这么做可以提高效率,空间换时间,就不用每次申请的长度调整到8的倍数
                               
                       
               
               
                       
                                函数:
                                SHH_MA() { xPrintf("SHH_MA构造\n"); init(); }
                                构造函数,在被声明为私有,是为了完成单例模式,因为整个程序只要有一个内存管理的就足够了,在构造内部调用了初始化函数

                                SHH_MA(const SHH_MA& SHH_MA) = delete;
                                SHH_MA& operator=(const SHH_MA& SHH_MA) = delete;
                                复制构造和赋值被显示禁用防止拷贝

                                static SHH_MA& SHH_MA_OBJ();
                                该函数在内部构造一个本类对象,使用时,通过该函数返回本类在其内部的唯一个单例对象,然后调用其他函数

                                void init();
                                在内部初始化freedom_list_map数组,和16个不同的长度内存池

                                void init_assit(int begin, int end, free_lists* p);
                                辅助init函数初始化,尽在init内部调用

                                void* request_memory(size_t size);
                                分配内存,返回void指针,返回内存就够了,在内存上干什么与这里无关

                                void return_memory(void *p);
                                回收内存

                               
                       
               
       


                               
        class free_lists;       
       
               
                       
                                数据:无

                       
               
               
                       
                                函数:
                                free_lists() {}
                                virtual ~free_lists() {}
                                构造析构略过

                                virtual void init(int init_block_size) { xPrintf("base init\n"); }                                        // 初始化
                                virtual void* request_memory() { xPrintf("base request\n"); return nullptr; }                // 申请内存
                                virtual void return_memory(void* p) { xPrintf("base return\n"); }                                // 归还内存
                                提供的虚函数接口, free_listor则会再次实现这些接口,这样通过在SHH_MA的free_lists数组,就可调用free_listor中对应的函数,还在SHH_MA易于管理
                               
                               
                       
               
       
        class free_listor:public free_lists{};
       
               
                       
                                数据:
                                        int md_init_block_size;                // 该链表的初始区块数量,举例:比如当前模板参数被8初始化,那么这个对象就有md_init_block_size块8字节的内存
                                        int md_block_size;                        // 链表没有可分配的空间时,会调用填充函数,再一次申请连续的内存块,初始化成链表,链入表中
                                        obj<n>* md_header;                        // 链表的头
                                        int md_one_offset;                        // 一次偏移的距离,为了防止多次计算
                                        std::mutex md_mutex;                // 多线程安全的保证
                                        char* POOL;                                // 初始化时,申请的一大块连续内存的初始地址
                               
                       
               
               
                       
                                函数:
                                        free_listor();
                                        构造并没有干什么,只是将值初始化成0和空指针

                                        virtual void init(int init_block_size);
                                        初始化,主要就是申请大块内存,初始化成链表                                       

                                        virtual void* request_memory();
                                        申请内存

                                        virtual void return_memory(void* p);
                                        释放内存
                       
                                        void fill_memory();
                                        two_request_memory();
                                        这两个函数只会被void* request_memory调用,当request_memory函数管理的链表为空时,就会调用 fill_memory函数填空链表,增加可分配的块,
                                        接着two_request_memory();就会再次申请内存,如果two_request_memory()再次申请内存失败,会直接调用malloc分配内存,malloc再失败就抛出异常

                               
                       
               
       
        union obj;
       
               
                       
                                数据:
                                为什么这个联合体是个模板,其实这个联合体才是整个最最核心的地方
                                md_data是为了定义整个区块的长度
                                而md_next则是为了方便把内存块转换成对象指针方便操作
                                其中md_data就是整个内存管理中最最精髓的地方,它既是链表,又是可分配的内存空间
                                举例:初始化了一个obj<8>的对象,那么根据md_data,这个obj对象实例就有8字节长,这个md_data:
                                        在链表中:使用md_next类型,存储指向下一个节点的地址
                                        被分配出去后:存储要写入的数据,让我用一张图来说明
                                md_header一开始指向地址为0的内存单元,而地址0中存储的数据是指向地址8的指针,而地址8开始存储的数据是指向 地址16的指针
                                当我分配出去一块8字节的内存后,md_header变更为指向地址8的位置,而地址0被返回给调用者,调用者在地址0开始的地方覆盖,直接写数据
                                地址0被归还回来的时候,接入链表,md_header又指向地址0,而地址0中的数据又被覆盖写为指向 地址8的指针
                                另:这里可能会有一个疑问?为什么最小分配的内存单元是8字节,因为这是为了兼容32位和64位系统,32位下的指针是4字节,64位下指针8字节
                                      32下可能会浪费4字节的空间,但64位下,8字节却是刚刚好
                               
                                                       
                       
               
               
                       
                                函数:无
                       
               
       


四、每个函数内部的逻辑
五、不足

花开自有丶花落 发表于 2019-7-19 16:09:58

没人吗

歪头 发表于 2019-7-23 17:39:19

变量info_stand是不是标记,代码中的意思标记是用来记录一个单位长度的内存块的大小的,但是好像没什么作用,因为不会连续分配内存块,会不会浪费了内存空间。回收内存的时候最好验证一下是不是内存池中的地址。

花开自有丶花落 发表于 2019-7-24 19:58:48

本帖最后由 花开自有丶花落 于 2019-7-24 20:02 编辑

歪头 发表于 2019-7-23 17:39
变量info_stand是不是标记,代码中的意思标记是用来记录一个单位长度的内存块的大小的,但是好像没什么作用 ...

这个标记是为了区分是哪个自由链表的,比如说n被初始化为8,然后多申请两字节的空间,转换成short,把这个short赋值成8,这样在归还的时候,通过这个short来判断是属于哪个free_listor的,如果是直接malloc分配的,也会多申请一个short,把哪个short赋值为-1代表不是内存池分配的,直接free
为了解决内存碎片当然需要付出一些代价

歪头 发表于 2019-7-24 21:39:32

花开自有丶花落 发表于 2019-7-24 19:58
这个标记是为了区分是哪个自由链表的,比如说n被初始化为8,然后多申请两字节的空间,转换成short,把 ...

有道理,思考了一下,确实没想到更好的办法。
页: [1]
查看完整版本: (原创)手撸一个简化版std::allocator内存池,附源码及解释