1.【C++】模仿tcmalloc从零实现一个高并发内存池(一)
【C++】模仿tcmalloc从零实现一个高并发内存池(一)
首先,源码本项目的源码原型是Google的一个开源项目TCMalloc,TCMalloc是源码谷歌开发的一种内存分配器(Memory Allocator),专门用于优化大规模的源码多线程应用程序的内存分配和管理。TCMalloc的源码全称是Thread-Caching Malloc,意为线程缓存内存分配器。源码高级分列源码它最初是源码为谷歌的服务器应用程序而开发的,旨在解决大规模多线程服务器应用程序中内存分配性能不佳的源码问题。
Google作为世界级大厂,源码学习并复现该项目,源码我们能体会到其精妙的源码项目架构设计。
1.内存池1.1 什么是源码内存池池化技术
池是在计算机技术中经常使用的一种设计模式,其内涵在于:将程序中需要经常使用的源码核心资源先申请出来,放到一个池内,源码黄线下4格买入指标源码由程序自己管理,源码这样可以提高资源的使用效率,也可以保证本程序占有的资源数量。经常使用的池技术包括内存池、线程池和连接池等,其中尤以内存池和线程池使用最多。
内存池(Memory Pool)是一种动态内存分配与管理技术,旨在优化内存的分配和释放过程,减少内存碎片化,提高程序和操作系统的性能。通常情况下,程序员直接使用诸如new、delete、玖五社区系统源码V9.8malloc、free等API进行内存的申请和释放,但这种方式容易导致内存碎片化问题,尤其在长时间运行的情况下性能表现更为明显。
内存池的工作原理是在程序启动时预先申请一大块内存作为池,之后在程序运行过程中,从这个内存池中动态分配内存给程序使用。当程序需要内存时,可以直接从内存池中取出一个空闲的内存块;当程序释放内存时,将释放的内存块重新放回内存池中,以备后续使用。同时,内存池会尽可能地与周边的淘宝优惠券移动端源码空闲内存块合并,以减少内存碎片的产生。如果内存池的空闲内存不足以满足程序需求,内存池会自动扩大,向操作系统申请更大的内存空间。
内存池的优点包括:
1.2 为什么需要内存池效率原因
假设你是一个图书管理员,你管理着一个图书馆的书籍。每当有读者来借书时,你需要为他们分配一本书。如果每次有读者来借书时都去印刷新书(动态分配内存),等读者归还书籍后再销毁书籍(释放内存),那么你将花费大量的时间和精力来管理这个过程,而且频繁的印刷和销毁书籍会带来一定的成本和效率损失。
相反,天津绿源码头怎么样如果你有一个固定数量的书库(内存池),这些书籍已经预先准备好并且处于待命状态。当读者来借书时,你只需要从书库中选择一本可用的书分配给他们,归还后书籍继续留在书库中等待下一个读者的到来。这样可以避免频繁的书籍印刷(内存分配)和销毁(内存释放),节省了时间和成本,提高了效率。
内存碎片问题
假设我们直接在堆上直接申请内存,依次申请出8ytes,bytes,bytes,bytes,此时堆上还剩8bytes没被申请,而这时候我们向堆归还了原先的bytes和bytes的内存,这时如果我们想再去堆上申请一个bytes的内存是申请不出来的,但是实际上我们此时空余的内存达到了bytes,但可惜的是我们无法凑出连续的bytes内存块。而这也就是所谓的外部内存碎片问题。
内存碎片问题通常被分为内部碎片问题和外部碎片问题。这两种碎片问题都是指在内存管理过程中未被有效利用的内存空间,但它们的来源和解决方法略有不同。
1.3 实现定长内存池理解池化技术
malloc
在C/C++编程中,当我们需要动态分配内存时,确实不是直接与操作系统打交道,而是通过诸如malloc这样的函数来完成。malloc是一个C标准库提供的函数,它帮助我们从计算机系统的堆内存区域请求一定数量的连续内存空间。
在C++中,new关键字则是用来进行动态内存分配的另一种方式,它本质上是对malloc功能的扩展和封装,不仅分配内存,还能自动调用构造函数初始化对象(对于类类型数据)。当你使用new分配数组或对象时,它会确保每个元素或对象都被正确初始化。而delete关键字在释放内存的同时,也会调用析构函数来清理对象。
不同编译器和操作系统环境下,malloc的具体实现可能会有所不同。例如,在Windows下的Visual Studio编译器中,malloc由微软进行了特定的优化实现;而在Linux系统下,采用glibc库的编译器(如GCC)则使用ptmalloc作为默认的malloc实现。这些实现通常包含高级算法,旨在提高内存分配的效率、减少碎片并满足各种内存需求。
定长内存池的设计
malloc函数作为一个通用的内存分配器,它可以接受任意大小的内存请求,并试图在堆上找到合适的连续空间进行分配。然而,由于它的通用性,它必须处理各种尺寸的请求,这可能导致内部碎片(即分配出的内存块之间存在无法使用的空隙),并且每次分配或释放都需要一定的查找和维护成本,所以对于特定场景而言,其性能可能不如针对性设计的内存管理方案。
定长内存池(Fixed-size Memory Pool)正是这样一种针对性的设计,它适用于那些内存块大小固定的场景,例如在大量创建和销毁相同大小对象的应用中。在定长内存池中,内存预先按照固定大小进行划分,申请和释放内存的操作可以简化为从池中取出或归还一个预设大小的内存块,无需像malloc那样复杂的寻找合适大小的空间。这种做法能够显著减少内存分配和释放的开销,消除内部碎片。
通过从零实现一个定长内存池,我们可以更直接的理解池化技术,同时也是在为高并发内存池项目的实现做铺垫。
我们首先写出定长内存池类ObjMemoryPool的大致框架,同时使用可变模板参数class T表示定长内存池存储的对象的类型和long long Nums来表示每次内存池预先申请多少个T大小的空间。
同时我们后面会通过一个_freelist来实现对回收对象的管理,_remainder则表示内存池剩余的字节数,同时我们会留出接口New(),Delete(),来调用以申请和销毁对象。
_freelist的设计
这里我们将_freelist(空闲链表)设计成一个不带头的单向链表,把每个回收的对象当作链表的节点链接存储维护。
我们会从每一个对象的内存块中取出一块来存放下一个内存块的地址来实现指针链接
但是这个时候我们会遇到一个问题:指针的大小在位和位下的大小不一致,如何在位平台取出4个字节,而在位平台下取出8个字节呢?
这里通过将当前obj对象的指针强转为void**类型,即认为obj指向一个指针类型,这时候直接解引用该指针就一定是取出一个指针大小的内存块,我们就可以在其中读取或者写入下一个内存块的地址。
New()和Delete()的设计
由于是_freelist是单向链表,这里我们将节点的插入与取出都设计成O(1)时间复杂度的头插和头删。
Delete()
Delete()接口的实现最简单,我们只需要去调用内存块中对应类型的析构函数,再直接将其头插进_freelist里面即可。
直接向堆申请空间
这里我们的项目将摆脱与malloc的联系,直接向堆申请空间使用和管理。
要想直接向堆申请内存空间,在Windows下,可以调用VirtualAlloc函数;在Linux下,可以调用brk或mmap函数。这里我在Windows下便选择使用VirtualAlloc函数。
_RoundUp()对齐
既然我们是选择以页为单位申请内存块, 这里我们在申请内存前就要将申请的内存大小向上取整对齐页的大小,这里的alignsize使用时就传入一页的字节大小。
New()
与malloc()做性能对比
完整代码:
1)定义了一个简单的二叉树节点结构 TreeNode,做为被测试申请释放的obj类型
2)在 main() 函数中:
3)录了每组实验的开始和结束时间,并输出两组实验的时间差,以比较它们的性能。最后输出了两组实验的耗时情况。
测试结果:
这里我们可以明显看到,此时定长内存池的申请释放效率明显快过malloc和free