(原创)手撸一个简化版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字节却是刚刚好
函数:无
四、每个函数内部的逻辑
五、不足 没人吗
变量info_stand是不是标记,代码中的意思标记是用来记录一个单位长度的内存块的大小的,但是好像没什么作用,因为不会连续分配内存块,会不会浪费了内存空间。回收内存的时候最好验证一下是不是内存池中的地址。 本帖最后由 花开自有丶花落 于 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 19:58
这个标记是为了区分是哪个自由链表的,比如说n被初始化为8,然后多申请两字节的空间,转换成short,把 ...
有道理,思考了一下,确实没想到更好的办法。
页:
[1]