本篇摘要在本篇将介绍C定长内存池的概念及实现问题引入内存池技术通过实现一个简单的定长内存池部分体会奥妙所在进而为之后实现整体的内存池做铺垫一· 高并发内存池概述⾼并发的内存池他的原型是google的⼀个开源项⽬tcmalloctcmalloc全称Thread-Caching Malloc即线程缓存的malloc实现了⾼效的多线程内存管理⽤于替代系统的内存分配相关的函数malloc、free。二· 什么是内存池️池化技术所谓“池化技术”就是程序先向系统申请过量的资源然后⾃⼰管理以备不时之需。也就是提前申请好对应的大内存以防止每次申请开销过大导致程序运行效率过慢的问题。“池的分类”内存池连接池线程池对象池等。以服务器上的线程池为例它的主要思想是先启动若⼲数量的线程让它们处于睡眠状态当接收到客⼾端的请求时唤醒池中某个睡眠的线程让它来处理客⼾端的请求当处理完这个请求线程⼜进⼊睡眠状态。☀️内存池☀️内存池是指程序预先从操作系统申请⼀块⾜够⼤内存此后当程序中需要申请内存的时候不是直接向操作系统申请⽽是直接从内存池中获取。同理当程序释放内存的时候并不真正将内存返回给操作系统⽽是返回内存池。当程序退出(或者特定时间)时内存池才将之前申请的内存真正释放。内存池用来解决的问题解决效率问题以及内存碎片问题。何为内存碎片内存碎⽚分为外碎⽚和内碎⽚。外部碎⽚是⼀些空闲的连续内存区域太⼩这些内存空间不连续以⾄于合计的内存⾜够但是不能满⾜⼀些的内存分配申请需求。如图此时由于vector和list对象销毁释放的空间不连续如果来了个更大的对象虽然有384字节空间但是申请超过256字节的对象就无法申请了就是因为这俩块碎片化导致不连续了那么此时这段空间就无法被正常分配就是有外部碎片导致内部碎⽚是由于⼀些对⻬的需求导致分配出去的空间中⼀些内存⽆法被利⽤。 再识mallocC/C中我们要动态申请内存都是通过malloc去申请内存但是我们要知道实际我们不是直接去堆获取内存不是直接去堆获取内存的。这里可以把malloc理解成一个内存池malloc()相当于向操作系统“批发”了⼀块较⼤的内存空间然后“零售”给程序⽤。当全部“售完”或程序有⼤量的内存需求时再根据实际需求向操作系统“进货”。对于malloc实现不同平台底层实现方法不一样windows的vs中用的微软自己实现的linux的gcc⽤的glibc中的ptmalloc。三· 定长内存池何为定长内存池简单理解成就是每次申请的一个对象的长度都是固定的一种类型的定长内存池只能一直申请一种类型的对象。如图而我们之前的常使用的malloc是这样的通过对比我们可以发现正是因为一种类型定长内存池只能申请一种类型的对象空间这就给了这种申请方式很多优点比如解决了外部碎片问题因为定长内存池每次申请释放都是指定相同类型对象大小而每次申请也是相同对象大小故外部碎片可以重新被利用。其次就是不用了的空间放在freelist链表指定类型对象大小的整数倍中等到需要的时候可以及时被利用就不用再次申请空间进而导致效率底下问题了。定长内存池设计设计思路提供New与Delete接口来代替C库中的new与delete即采取底层模拟调用new与delete的底层来实现简单版本的接口。首先对于变量设计的有三个memory大块未利用的内存首部的指针remainbytes大块内存剩余字节数freelist被回收的T对象内存块的地址链表。对于memory这也就是我们malloc出的大块的定长内存块每次从这里面取T对象大小的块或者从freelist里面取曾经被释放的接着利用。对于freelist这里我们想要被回收的内存块也就是首地址能及时被找到然后再次利用因此采取把对应回收内存块的首地址用链表形式管理起来也就是每个内存块里面前4/8个字节保存一下下一个内存块的首地址这样就连接起来了。对于remainbytes这里就不用多说了就是大块内存剩余的空间如果不足了对应一个T对象大小了就在开一块大的定长内存块等到原先这块有新的内存块被释放在用这块也就是freelist里的内存块随着T对象大小内存块不断增多就形成了管理多个大块定长内存块分部分使用的模式。形象理解一下这个过程但是实际并不是这样的整体都是在这一个开辟的定长内存块上操作的只不过我们把它拆开当成链表来理解这个大内存块以及回收的块比较好理解而已。️Delete接口设计这里我们只需要把对应的回收的块的地址链表形式保存即可但是有个问题就是不同平台地址大小是不同的有种简单的方式就是进行判断代码语言javascriptAI代码解释//直接判断进行放入 if (sizeof(T*) 4) *(int*)obj (int)_freelist; else *(long long*)obj (long long) _freelist; _freelist obj;当然了这种是比较漏的下面我们可以利用不同平台指针大小随它自己变化的特点也就是取前指针大小字节进行直接赋值无需强转的代码语言javascriptAI代码解释*(void**)obj _freelist; _freelist obj;这里不一定只能是void **类型int **等都是可以的因为它们对应解引用得到的一级指针大小都是对应平台指针大小也就是说是可以自己适应的。️Delete 代码代码语言javascriptAI代码解释void Delete(T*obj) { //这里把回收的内存块的首地址保存以链表的形式串起来每个地址都是拿到一个T类型大小对象 obj-~T();//显示调用析构 //下面两种方法进行因为不同平台下地址大小是不同的简单的就是直接判断然后强转放入另一种就是利用不同平台下指针大小随平台变化来存入 //直接判断进行放入 //if (sizeof(T*) 4) *(int*)obj (int)_freelist; //else *(long long*)obj (long long) _freelist; //32为平台一级地址就是4字节64位就是8字节二级指针解引用对应的空间放一级指针大小就是对应字节了 *(void**)obj _freelist; _freelist obj; }️New接口设计思路首先先去对应的freelist里面看看是否有空余的内存块因为每次只有被释放掉才会用它连接起来因此只要不为空一定够一个对象大小的。其次再去对应的大块内存块是否remainbytes还够用如果不够用就新开辟大的定长内存块用否则就直接用然后调节对应的memory指针位置以及remainbytes大小即可。但是还有个问题如果申请的T对象是类似charshort等少于四个字节呢那么如果释放后把它挂入freelist里不就取不到对应的空间来放地址了因此我们最少也要New对应平台下指针大小空间。代码语言javascriptAI代码解释//如果T对象不足对应的平台下的地址的大小那么后面在内存块中取前4/8个字节的时候就出错了因此如果不够最小也要满足这个条件 size_t objsize sizeof(T) sizeof(T*) ? sizeof(T*) : sizeof(T);还有就是我们模拟的是new的底层因此还会需要显示调用定位new初始化一下代码语言javascriptAI代码解释//显示调用T对象的默认构造函数 new(obj)T();还有个小坑当写New的时候无脑的选择了如果remainbytes不够了直接VirtualFree没考虑第一次memory为nullptr就直接释放了的情况正是因为这个bug导致了还没申请即崩了其次就是注意malloc申请的采用freeVirtualAlloc申请的用VirtualFree因此需要注意代码语言javascriptAI代码解释if (!_memory)VirtualFree(_memory, 0, MEM_RELEASE); //这里第一次不能VirtualFree否则非法访问释放后面不足T对象大小的空间️New代码下面就写成了这样代码语言javascriptAI代码解释T* New() { //这里如果freelist里面不为空也就是有被回收的内存那么再次利用的话拿到对应的地址所对应的连续的内存块一定是 //一个T对象内存的指定类型对象大小的整数倍 T* obj nullptr; //回收的内存的链表如果有内存先利用 if (_freelist) { void* next *((void**)_freelist); obj (T*)_freelist; _freelist next; } else { //判断是否大块内存中还足够一个T对象大小 if (_remainbytes sizeof(T)) { //后面直接用memory即可 } //不足就重新申请 else { if (!_memory)VirtualFree(_memory, 0, MEM_RELEASE); //这里第一次不能VirtualFree否则非法访问释放后面不足T对象大小的空间 size_t allocbytes 128 * 1024; char* ptr (char*)malloc(allocbytes);//直接字节申请 if (!ptr) { throw std::bad_alloc(); } _memory ptr; _remainbytes allocbytes; } obj (T*)_memory; //如果T对象不足对应的平台下的地址的大小那么后面在内存块中取前4/8个字节的时候就出错了因此如果不够最小也要满足这个条件 size_t objsize sizeof(T) sizeof(T*) ? sizeof(T*) : sizeof(T); _memory objsize;//这里必须是char*如果是void*后就不知道加几个 _remainbytes - objsize; } //显示调用T对象的默认构造函数 new(obj)T(); return obj; }✅ 优化按页申请内存对于使用的64位机器默认一页就是4KB我们只需要修改下每次都按照页交给对应的alloc函数然后自己转化成字节进行申请即可这里我们按一页8KB算与后面PageCache的一页8KB相照应。下面需要用到一个函数VirtualAlloc也就是对应windows来说malloc底层调用的函数VirtualAlloc是Windows操作系统提供的一个内存管理函数它允许程序以页面为单位来保留、提交、更改或释放虚拟内存区域。这个函数对于需要精细控制其内存使用的应用程序特别有用。代码语言javascriptAI代码解释LPVOID VirtualAlloc( LPVOID lpAddress, SIZE_T dwSize, DWORD flAllocationType, DWORD flProtect );参数介绍1·lpAddress:指向希望分配的内存区域起始地址的指针。如果此值NULL操作系统将自动选择一个合适的地址。2·dwSize:要分配的内存大小以字节为单位。3·flAllocationType:分配类型标志可以是以下值之一或组合MEM_COMMIT: 为特定的页面区域分配实际物理存储器并将其标记为可用。MEM_RESERVE: 保留一个区域以备将来使用但不会分配任何物理存储。MEM_RESET: 表示将要废弃的内容操作系统可能会丢弃这些内容但不会立即执行此操作。其他如MEM_LARGE_PAGES,MEM_PHYSICAL等高级选项也可能适用。4·flProtect:内存保护选项在内存被提交时生效。常见的选项包括PAGE_READONLY: 只读访问。PAGE_READWRITE: 读写访问。PAGE_EXECUTE: 执行访问。PAGE_EXECUTE_READ: 执行和读取访问。PAGE_EXECUTE_READWRITE: 执行、读取和写入访问。成功时返回分配区域的起始地址失败时返回NULL并可通过GetLastError获取详细错误信息。上面了解下即可我们就下面修改下按照这个函数申请大块定长内存即可。