在 C++ 编程的世界里,频繁地进行内存分配与释放操作,就像是一场无休无止的 “资源争夺战”。每次使用 new 和 delete,不仅消耗大量的 CPU 时间在系统调用上,还极易引发内存碎片化,让程序性能大打折扣。想象一下,一个大型游戏在关键时刻,因为内存管理的低效卡顿,玩家体验瞬间崩塌;或是一个高频交易系统,因内存分配延迟错失良机。而今天,我们即将开启一段神奇之旅 —— 用 C++ 代码打造一个高性能内存池,将这些困扰一扫而空,让程序如虎添翼,掌控内存的高效利用。
一、引言(Introduction)
1.1 内存管理困境,你中招了吗?
在 C++ 编程的世界里,内存管理犹如一场精细的舞蹈,一步踏错,就可能引发性能的 “滑铁卢”。当我们使用默认的内存管理函数,如 new/delete 或 malloc/free 在堆上分配和释放内存时,一些隐藏的问题正悄然滋生。
想象一下,你正在开发一款实时图形渲染引擎,每一帧都需要快速地创建和销毁大量的图形对象。使用 new 来分配这些对象的内存,系统首先得在内部维护的内存空闲块表中,依据特定算法查找合适的空闲内存块。要是找到的块比需求大,还得切割,之后更新表格,这一系列操作带来了不小的额外开销。就好比快递员在一个杂乱无章的仓库里找包裹,每次都得花费大量时间翻找、整理,效率自然高不起来。
频繁地分配与释放不同大小的内存块,还会导致内存碎片问题。这些碎片如同城市中零散分布、无法利用的小块空地,夹杂在已分配的内存之间,使得后续较大内存需求难以得到满足。即使系统试图合并相邻空闲块,但面对复杂的内存使用情况,也常常有心无力。最终,内存利用率大打折扣,程序运行起来越来越慢,就像道路被杂物堵塞,交通逐渐瘫痪。
更令人头疼的是内存泄漏。倘若在代码的某个角落,new 出来的内存忘记用 delete 释放,这就好比打开水龙头后没关紧,水(内存)一直在流,却没有回收的机制。随着程序运行,可用内存越来越少,直到系统不堪重负,甚至崩溃,辛苦构建的程序大厦可能因这一小小的 “疏忽” 轰然倒塌。
1.2 内存池登场,救星来了!
就在传统内存管理陷入困境之时,内存池宛如一位身披铠甲的骑士闪亮登场。内存池,简单来说,是在程序启动之初,就预先申请一块较大的连续内存区域,就好比提前圈出一大片专属的 “内存领地”。这片领地被精心划分成一个个大小相等(通常情况下)的小内存块,犹如整齐排列的储物格。
当程序运行过程中需要分配内存时,直接从这片内存池中 “提货”,也就是取出合适的内存块交付使用,而无需频繁向操作系统 “求情” 索要内存。就像超市有自己的仓库提前备好货物,顾客来买东西时直接从仓库取,比每次缺货都临时向供应商进货快多了。如此一来,省去了系统在内部空闲块表中反复查找、切割、更新的繁琐流程,内存分配速度大幅提升。
由于内存池分配的是预先划分好的固定大小块,避免了像传统方式那样随意切割内存造成的碎片,内存空间得以规整排列,利用率大大提高。而且,内存池通常设有回收机制,当某个内存块使用完毕,会被精准地归还到池中,等待下次复用,就像图书馆的书被借走看完后又还回书架,而不是被随意丢弃。这一机制有效杜绝了因忘记释放内存而导致的泄漏问题,让程序的内存管理更加稳健、可靠,为长时间稳定运行保驾护航。
二、内存池技术详解
2.1 什么是内存池
内存池(Memory Pool),也被称为对象池(Object Pool),是一种内存管理策略。在这种策略中,内存被划分为固定大小的块,这些块被组织在一起并被称为“池”。当程序需要分配内存时,它会从内存池中获取一个块,而不是直接从操作系统请求内存。
内存池的主要优点是它可以减少内存分配和释放的开销。因为内存池中的块是预先分配的,所以分配内存只需要从池中取出一个块,而不需要进行系统调用。同样,释放内存只需要将块放回池中,而不需要通知操作系统。这种方式可以大大提高内存分配和释放的速度。
此外,内存池还可以减少内存碎片化。因为所有的块都是相同大小的,所以它们可以紧密地排列在一起,而不会留下无法使用的空隙。这种方式可以提高内存的利用率,特别是对于那些需要大量小块内存的程序。然而,内存池并不是万能的。它也有一些缺点,比如它不能很好地处理大块内存的分配,因为大块内存的分配可能会导致内存池中的空间被浪费。此外,内存池的管理也会带来一些开销,特别是当内存池需要扩展或收缩时。
总的来说,内存池是一种强大的工具,它可以帮助我们更有效地管理内存。但是,像所有工具一样,我们需要理解它的优点和缺点,以便在适当的情况下使用它。
图片
2.2 为什么需要内存池
⑴内存碎片问题
造成堆利用率很低的一个主要原因就是内存碎片化。如果有未使用的存储器,但是这块存储器不能用来满足分配的请求,这时候就会产生内存碎片化问题。内存碎片化分为内部碎片和外部碎片。
内部碎片:内部碎片是指一个已分配的块比有效载荷大时发生的。(假设以前分配了10个大小的字节,现在只用了5个字节,则剩下的5个字节就会内碎片)。内部碎片的大小就是已经分配的块的大小和他们的有效载荷之差的和。因此内部碎片取决于以前请求内存的模式和分配器实现(对齐的规则)的模式。
外部碎片:假设系统依次分配了16byte、8byte、16byte、4byte,还剩余8byte未分配。这时要分配一个24byte的空间,操作系统回收了一个上面的两个16byte,总的剩余空间有40byte,但是却不能分配出一个连续24byte的空间,这就是外碎片问题。
图片
⑵申请效率问题
例如:我们上学家里给生活费一样,假设一学期的生活费是6000块。
- 方式1:开学时6000块直接给你,自己保管,自己分配如何花。
- 方式2:每次要花钱时,联系父母,父母转钱。
同样是6000块钱,第一种方式的效率肯定更高,因为第二种方式跟父母的沟通交互成本太高了。
同样的道理,程序就像是上学的我们,操作系统就像父母,频繁申请内存的场景下,每次需要内存,都像系统申请效率必然有影响。
三、C++内存管理机制
3.1 C++内存分配与释放
在C++中,内存的分配和释放是一个非常重要的环节。理解这个过程,对于我们深入理解内存池的设计与实现,有着至关重要的作用。
首先,我们来看一下C++中的内存分配。在C++中,我们通常使用new操作符来分配内存。当我们写下如下代码:
int* p = new int;
这行代码做了什么呢?首先,new操作符会向操作系统请求一块内存,大小为一个int的大小。如果请求成功,操作系统会返回这块内存的地址,然后new操作符会将这个地址赋值给指针p。这样,我们就成功地在堆(Heap)上分配了一块内存。
那么,这个过程中有什么需要我们注意的地方呢?首先,我们需要注意的是,new操作符的执行效率。因为new操作符需要向操作系统请求内存,这个过程涉及到了系统调用,是一个相对耗时的操作。因此,如果我们在程序中频繁地使用new操作符,可能会导致程序的执行效率降低。
其次,我们需要注意的是,new操作符可能会失败。当操作系统的可用内存不足时,new操作符会返回一个空指针。因此,我们在使用new操作符时,需要检查其返回值,以防止内存分配失败。
那么,我们如何释放内存呢?在C++中,我们使用delete操作符来释放内存。当我们写下如下代码:
delete p;
这行代码做了什么呢?delete操作符会将p指向的内存块返回给操作系统,这样,这块内存就可以被其他程序使用了。同时,为了防止产生野指针,delete操作符会将p的值设置为nullptr。
在使用delete操作符时,我们需要注意的是,必须确保要删除的指针是由new操作符分配的。如果我们试图删除一个非法的指针,或者是一个已经被删除的指针,都会导致未定义的行为。此外,我们还需要注意,如果我们使用new[]操作符分配了一个数组,那么在删除这个数组时,必须使用delete[]操作符,而不能使用delete操作符。
总的来说,C++中的内存分配和释放是一个相对复杂的过程,需要我们仔细处理。在接下来的章节中,我们将看到,内存池可以帮助我们简化这个过程,提高程序的执行效率,同时也可以帮助我们更好地管理内存,防止内存泄漏和野指针的产生。
3.2 C++内存管理的问题
虽然C++提供了new和delete操作符来帮助我们管理内存,但在实际使用中,我们仍然会遇到一些问题。这些问题主要包括:
- 内存碎片(Memory Fragmentation):在C++中,频繁地进行内存的分配和释放,会导致内存碎片的产生。内存碎片是指一些小的、无法被有效利用的内存块。这些内存块虽然无法被有效利用,但仍然会占用系统资源,降低系统的性能。
- 内存泄漏(Memory Leak):在C++中,如果我们分配了内存,但忘记释放,就会导致内存泄漏。内存泄漏会导致系统的可用内存不断减少,严重时甚至可能导致系统崩溃。
- 野指针(Dangling Pointer):在C++中,如果我们释放了一块内存,但仍然有指针指向这块内存,这个指针就成为了野指针。野指针是非常危险的,因为我们无法预测对野指针的操作会有什么后果。
- 分配和释放内存的效率问题:在C++中,分配和释放内存需要调用操作系统的函数,这是一个相对耗时的操作。如果我们在程序中频繁地分配和释放内存,可能会导致程序的执行效率降低。
以上就是在C++中进行内存管理时可能会遇到的一些问题。在接下来的章节中,我们将看到,内存池可以帮助我们解决这些问题,提高程序的执行效率,同时也可以帮助我们更好地管理内存,防止内存泄漏和野指针的产生。
3.3 内存池解决的问题
内存池(Memory Pool)是一种内存管理策略。通过预先在内存中分配一大块连续的内存空间,然后将这块内存空间划分为大小相等的小块,当程序需要分配内存时,直接从内存池中分配一块小内存,而不是直接向操作系统申请。这种方式可以有效地解决C++内存管理中的一些问题。
- 解决内存碎片问题:因为内存池中的内存块大小是固定的,所以不会出现因为频繁分配和释放不同大小的内存导致的内存碎片问题。
- 提高内存分配效率:内存池在程序启动时就已经预先分配了一大块内存,所以在程序运行过程中分配和释放内存的速度会比直接使用new和delete操作符快很多。
- 防止内存泄漏和野指针:内存池通常会提供一些机制来跟踪内存的使用情况,比如引用计数等,这可以帮助我们更好地管理内存,防止内存泄漏和野指针的产生。
四、内存池的设计
在C/C++中我们通常使用malloc,free或new,delete来动态分配内存。一方面,因为这些函数涉及到了系统调用,所以频繁的调用必然会导致程序性能的损耗;另一方面,频繁的分配和释放小块内存会导致大量的内存碎片的产生,当碎片积累到一定的量之后,将无法分配到连续的内存空间,系统不得不进行碎片整理来满足分配到连续的空间,这样不仅会导致系统性能损耗,而且会导致程序对内存的利用率低下。
当然,如果我们的程序不需要频繁的分配和释放小块内存,那就没有使用内存池的必要,直接使用malloc,free或new,delete函数即可。
- malloc优点:使用自由链表的数组,提高分配释放效率;减少内存碎片,可以合并空闲的内存
- malloc缺点:为了维护隐式/显示链表需要维护一些信息,空间利用率不高;在多线程的情况下,会出现线程安全的问题,如果以加锁的方式解决,会大大降低效率。
4.1 为什么要使用内存池
- 解决内碎片问题
- 由于向内存申请的内存块都是比较大的,所以能够降低外碎片问题
- 一次性向内存申请一块大的内存慢慢使用,避免了频繁的向内存请求内存操作,提高内存分配的效率
- 但是内碎片问题无法避免,只能尽可能的降低
4.2 内存池的演变
⑴最简单的内存分配器:做一个链表指向空闲内存,分配就是取出一块来,改写链表,返回,释放就是放回到链表里面,并做好归并。注意做好标记和保护,避免二次释放,还可以花点力气在如何查找最适合大小的内存快的搜索上,减少内存碎片,有空你了还可以把链表换成伙伴算法。
- 优点: 实现简单
- 缺点: 分配时搜索合适的内存块效率低,释放回归内存后归并消耗大,实际中不实用。
⑵定长内存分配器:即实现一个 FreeList,每个 FreeList 用于分配固定大小的内存块,比如用于分配 32字节对象的固定内存分配器,之类的。每个固定内存分配器里面有两个链表,OpenList 用于存储未分配的空闲对象,CloseList用于存储已分配的内存对象,那么所谓的分配就是从 OpenList 中取出一个对象放到 CloseList 里并且返回给用户,释放又是从 CloseList 移回到 OpenList。分配时如果不够,那么就需要增长 OpenList:申请一个大一点的内存块,切割成比如 64 个相同大小的对象添加到 OpenList中。这个固定内存分配器回收的时候,统一把先前向系统申请的内存块全部还给系统。
- 优点: 简单粗暴,分配和释放的效率高,解决实际中特定场景下的问题有效。
- 缺点: 功能单一,只能解决定长的内存需求,另外占着内存没有释放。
⑶哈希映射的FreeList 池:在定长分配器的基础上,按照不同对象大小(8,16,32,64,128,256,512,1k…64K),构造十多个固定内存分配器,分配内存时根据要申请内存大小进行对齐然后查H表,决定到底由哪个分配器负责,分配后要在内存头部的 header 处写上 cookie,表示由该块内存哪一个分配器分配的,这样释放时候你才能正确归还。如果大于64K,则直接用系统的 malloc作为分配,如此以浪费内存为代价你得到了一个分配时间近似O(1)的内存分配器。这种内存池的缺点是假设某个 FreeList 如果高峰期占用了大量内存即使后面不用,也无法支援到其他内存不够的 FreeList,达不到分配均衡的效果。
- 优点:这个本质是定长内存池的改进,分配和释放的效率高。可以解决一定长度内的问题。
- 缺点:存在内碎片的问题,且将一块大内存切小以后,申请大内存无法使用。多线程并发场景下,锁竞争激烈,效率降低。
范例:sgi stl 六大组件中的空间配置器就是这种设计实现的。
4.3 内存池的设计原则
内存池(Memory Pool)的设计是一个复杂且需要深入理解计算机内存管理的过程。设计一个高效的内存池,我们需要遵循以下几个原则:
- 最小化内存分配次数:内存分配是一个开销较大的操作,频繁的内存分配和释放会导致系统性能下降。因此,内存池的设计应尽可能减少内存分配次数,一种常见的做法是预先分配一大块内存,然后在需要时从中划分出小块内存。
- 减少内存碎片:频繁的内存分配和释放会导致内存碎片,这会降低内存的利用率。内存池通过管理预先分配的内存,可以有效地减少内存碎片。
- 快速响应内存请求:内存池应能快速地响应内存请求,这需要内存池有一个高效的数据结构来管理可用的内存块。常见的做法是使用链表或者树形结构来管理内存块。
- 灵活的内存管理:内存池应能灵活地管理内存,包括内存的分配、释放和整理。这需要内存池有一套完整的内存管理策略。
以上就是设计内存池需要遵循的原则,接下来我们将详细介绍如何根据这些原则来设计和实现一个内存池。
五、内存池的实现方案
内存池的实现原理大致如下:提前申请一块大内存由内存池自己管理,并分成小片供给程序使用。程序使用完之后将内存归还到内存池中(并没有真正的从系统释放),当程序再次从内存池中请求内存时,内存池将池子中的可用内存片返回给程序使用。
我们在设计内存池的实现方案时,需要考虑到以下问题:
①内存池是否可以自动增长?
如果内存池的最大空间是固定的(也就是非自动增长),那么当内存池中的内存被请求完之后,程序就无法再次从内存池请求到内存。所以需要根据程序对内存的实际使用情况来确定是否需要自动增长。
②内存池的总内存占用是否只增不减?
如果内存池是自动增长的,就涉及到了“内存池的总内存占用是否是只增不减”这个问题了。试想,程序从一个自动增长的内存池中请求了1000个大小为100KB的内存片,并在使用完之后全部归还给了内存池,而且假设程序之后的逻辑最多之后请求10个100KB的内存片,那么该内存池中的900个100KB的内存片就一直处于闲置状态,程序的内存占用就一直不会降下来。对内存占用大小有要求的程序需要考虑到这一点。
③内存池中内存片的大小是否固定?
如果每次从内存池中的请求的内存片的大小如果不固定,那么内存池中的每个可用内存片的大小就不一致,程序再次请求内存片的时候,内存池就需要在“匹配最佳大小的内存片”和“匹配操作时间”上作出衡量。“最佳大小的内存片”虽然可以减少内存的浪费,但可能会导致“匹配时间”变长。
④内存池是否是线程安全的?
是否允许在多个线程中同时从同一个内存池中请求和归还内存片?这个线程安全可以由内存池来实现,也可以由使用者来保证。
⑤内存片分配出去之前和归还到内存池之后,其中的内容是否需要被清除?
程序可能出现将内存片归还给内存池之后,仍然使用内存片的地址指针进行内存读写操作,这样就会导致不可预期的结果。将内容清零只能尽量的(也不一定能)将问题抛出来,但并不能解决任何问题,而且将内容清零会消耗一定的CPU时间。所以,最终最好还是需要由内存池的使用者来保证这种安全性。
⑥是否兼容std::allocator?
STL标准库中的大多类都支持用户提供一个自定义的内存分配器,默认使用的是std::allocator,如std::string:
typedef basic_string<char, char_traits<char>, allocator<char> > string;
如果我们的内存池兼容std::allocator,那么我们就可以使用我们自己的内存池来替换默认的std::allocator分配器,如:
typedef basic_string<char, char_traits<char>, MemoryPoll<char> > mystring;
5.1 内存池的基本结构
内存池的基本结构主要包括两个部分:内存块(Memory Block)和内存块链表(Memory Block List)。下面我们将详细介绍这两个部分。
- 内存块(Memory Block):内存块是内存池中最基本的单位,每个内存块都有一个固定的大小。内存块的大小可以根据实际需求进行设置,但通常情况下,我们会设置多种大小的内存块,以满足不同的内存需求。
- 内存块链表(Memory Block List):内存块链表是用来管理内存块的数据结构。每个链表节点代表一个内存块,节点中包含了内存块的地址和状态信息。通过内存块链表,我们可以快速地找到一个可用的内存块,也可以在内存块被释放时,快速地将其加入到链表中。
在实际的设计中,我们还需要考虑到内存池的扩展性和灵活性。例如,我们可以设计一个动态的内存池,当内存池中的内存块不足时,可以动态地增加内存块。另外,我们也可以设计多级内存池,通过多级内存池,我们可以更好地管理不同大小的内存块,提高内存的利用率。
以上就是内存池的基本结构,接下来我们将详细介绍如何根据这个结构来实现一个内存池。
5.2 内存池的工作原理
⑴预分配策略
内存池的高效首先得益于其预分配策略。在程序启动之际,内存池依据对程序运行过程中内存需求的预估,向操作系统一次性申请一块容量较大的连续内存空间,这就好比提前为一场盛宴准备了充足的食材,集中采购显然比宾客陆续点餐时厨师一次次临时外出采购高效得多。
这块内存被划分为多个大小固定的内存块,以适配常见的内存分配需求。比如,对于一个频繁创建小型数据结构(如链表节点)的程序,内存池会划分出一批刚好能容纳单个链表节点的小内存块。当程序需要创建新节点时,直接从内存池中取出一个预先备好的小内存块,无需像使用 new 时那样,让操作系统在碎片化的空闲内存中艰难寻觅合适空间,再进行切割、分配等繁琐流程,大大节省了时间开销。
⑵内存块管理
内存池内部精心维护着一个空闲内存块链表,这是它实现高效分配与回收的关键 “账本”。链表中的每个节点对应一个空闲内存块,它们通过指针相互串联,犹如一条无形的链条串起了内存池中的空闲资源。
当程序请求分配内存时,内存池沿着链表快速遍历,找到首个可用的空闲内存块,将其从链表中摘下交付给程序使用。这个过程如同从挂满钥匙的钥匙环上迅速取下一把所需的钥匙,速度极快。而当内存使用完毕,需要回收时,回收的内存块会被精准插回空闲链表,通常是插在表头位置,以便下次分配时能优先被选用,就像归还的钥匙总是放在最顺手可取的地方,方便下次快速找到。
由于内存块大小固定,不会出现因随意切割导致的零碎空间,避免了内存碎片的产生。而且,回收再利用机制使得内存利用率大幅提升,曾经被用过的内存块能迅速满血复活,重新投入战斗,为程序持续稳定运行提供坚实保障。
⑶多线程适配
在当今多核处理器盛行,多线程编程广泛应用的时代,内存池也具备出色的多线程适配能力。当多个线程同时对内存池发起内存分配或回收请求时,冲突风险骤升,就像多个人同时争抢有限的资源容易乱套。
为应对这一挑战,内存池引入了互斥锁等同步机制。当一个线程进入内存池执行分配或回收操作时,会先获取互斥锁,将内存池 “锁住”,其他线程此时只能等待,如同排队上公共厕所,门被占用时其他人只能在外等候。线程完成操作后,释放互斥锁,让其他线程有机会进入。
有些高性能内存池还采用更精细的无锁编程技术,利用原子操作等手段减少线程等待时间,进一步提升并发性能,确保在多线程的复杂环境下,内存池依然能有条不紊地运行,为各个线程快速、稳定地提供内存服务。
5.3 C++实现内存池的步骤
实现一个内存池需要经过以下几个步骤:
- 预分配内存:首先,我们需要预先分配一大块内存,这个内存的大小可以根据实际需求进行设置。预分配的内存将被划分为多个内存块,每个内存块的大小也可以根据需求进行设置。
- 初始化内存块链表:然后,我们需要初始化内存块链表。链表中的每个节点代表一个内存块,节点中包含了内存块的地址和状态信息。初始化内存块链表的过程就是将预分配的内存划分为多个内存块,并将这些内存块加入到链表中。
- 实现内存分配函数:内存分配函数是用来分配内存的,当有内存请求时,内存分配函数会从内存块链表中找到一个可用的内存块,并返回其地址。在这个过程中,我们需要考虑内存块的状态,只有状态为可用的内存块才能被分配。
- 实现内存释放函数:内存释放函数是用来释放内存的,当有内存被释放时,内存释放函数会将对应的内存块加入到内存块链表中,并将其状态设置为可用。在这个过程中,我们需要考虑内存块的状态,只有状态为已分配的内存块才能被释放。
- 实现内存整理函数:内存整理函数是用来整理内存的,它可以将连续的可用内存块合并为一个大的内存块,也可以将大的内存块划分为多个小的内存块。通过内存整理,我们可以提高内存的利用率,减少内存碎片。
以上就是实现内存池需要经过的步骤,每个步骤都需要深入理解内存管理的原理,才能设计出一个高效的内存池。
5.4 内存池的具体实现
计划实现一个内存池管理的类MemoryPool,它具有如下特性:
- 内存池的总大小自动增长。
- 内存池中内存片的大小固定。
- 支持线程安全。
- 在内存片被归还之后,清除其中的内容。
- 兼容std::allocator。
因为内存池的内存片的大小是固定的,不涉及到需要匹配最合适大小的内存片,由于会频繁的进行插入、移除的操作,但查找比较少,故选用链表数据结构来管理内存池中的内存片。
MemoryPool中有2个链表,它们都是双向链表(设计成双向链表主要是为了在移除指定元素时,能够快速定位该元素的前后元素,从而在该元素被移除后,将其前后元素连接起来,保证链表的完整性):
- data_element_ 记录以及分配出去的内存片。
- free_element_ 记录未被分配出去的内存片。
MemoryPool实现代码
代码中使用了std::mutex等C++11才支持的特性,所以需要编译器最低支持C++11:
#ifndef PPX_BASE_MEMORY_POOL_H_
#define PPX_BASE_MEMORY_POOL_H_
#include <climits>
#include <cstddef>
#include <mutex>
namespace ppx {
namespace base {
template <typename T, size_t BlockSize = 4096, bool ZeroOnDeallocate = true>
class MemoryPool {
public:
/* Member types */
typedef T value_type;
typedef T* pointer;
typedef T& reference;
typedef const T* const_pointer;
typedef const T& const_reference;
typedef size_t size_type;
typedef ptrdiff_t difference_type;
typedef std::false_type propagate_on_container_copy_assignment;
typedef std::true_type propagate_on_container_move_assignment;
typedef std::true_type propagate_on_container_swap;
template <typename U> struct rebind {
typedef MemoryPool<U> other;
};
/* Member functions */
MemoryPool() noexcept;
MemoryPool(const MemoryPool& memoryPool) noexcept;
MemoryPool(MemoryPool&& memoryPool) noexcept;
template <class U> MemoryPool(const MemoryPool<U>& memoryPool) noexcept;
~MemoryPool() noexcept;
MemoryPool& operator=(const MemoryPool& memoryPool) = delete;
MemoryPool& operator=(MemoryPool&& memoryPool) noexcept;
pointer address(reference x) const noexcept;
const_pointer address(const_reference x) const noexcept;
// Can only allocate one object at a time. n and hint are ignored
pointer allocate(size_type n = 1, const_pointer hint = 0);
void deallocate(pointer p, size_type n = 1);
size_type max_size() const noexcept;
template <class U, class... Args> void construct(U* p, Args&&... args);
template <class U> void destroy(U* p);
template <class... Args> pointer newElement(Args&&... args);
void deleteElement(pointer p);
private:
struct Element_ {
Element_* pre;
Element_* next;
};
typedef char* data_pointer;
typedef Element_ element_type;
typedef Element_* element_pointer;
element_pointer data_element_;
element_pointer free_element_;
std::recursive_mutex m_;
size_type padPointer(data_pointer p, size_type align) const noexcept;
void allocateBlock();
static_assert(BlockSize >= 2 * sizeof(element_type), "BlockSize too small.");
};
template <typename T, size_t BlockSize, bool ZeroOnDeallocate>
inline typename MemoryPool<T, BlockSize, ZeroOnDeallocate>::size_type
MemoryPool<T, BlockSize, ZeroOnDeallocate>::padPointer(data_pointer p, size_type align)
const noexcept {
uintptr_t result = reinterpret_cast<uintptr_t>(p);
return ((align - result) % align);
}
template <typename T, size_t BlockSize, bool ZeroOnDeallocate>
MemoryPool<T, BlockSize, ZeroOnDeallocate>::MemoryPool()
noexcept {
data_element_ = nullptr;
free_element_ = nullptr;
}
template <typename T, size_t BlockSize, bool ZeroOnDeallocate>
MemoryPool<T, BlockSize, ZeroOnDeallocate>::MemoryPool(const MemoryPool& memoryPool)
noexcept :
MemoryPool() {
}
template <typename T, size_t BlockSize, bool ZeroOnDeallocate>
MemoryPool<T, BlockSize, ZeroOnDeallocate>::MemoryPool(MemoryPool&& memoryPool)
noexcept {
std::lock_guard<std::recursive_mutex> lock(m_);
data_element_ = memoryPool.data_element_;
memoryPool.data_element_ = nullptr;
free_element_ = memoryPool.free_element_;
memoryPool.free_element_ = nullptr;
}
template <typename T, size_t BlockSize, bool ZeroOnDeallocate>
template<class U>
MemoryPool<T, BlockSize, ZeroOnDeallocate>::MemoryPool(const MemoryPool<U>& memoryPool)
noexcept :
MemoryPool() {
}
template <typename T, size_t BlockSize, bool ZeroOnDeallocate>
MemoryPool<T, BlockSize, ZeroOnDeallocate>&
MemoryPool<T, BlockSize, ZeroOnDeallocate>::operator=(MemoryPool&& memoryPool)
noexcept {
std::lock_guard<std::recursive_mutex> lock(m_);
if (this != &memoryPool) {
std::swap(data_element_, memoryPool.data_element_);
std::swap(free_element_, memoryPool.free_element_);
}
return *this;
}
template <typename T, size_t BlockSize, bool ZeroOnDeallocate>
MemoryPool<T, BlockSize, ZeroOnDeallocate>::~MemoryPool()
noexcept {
std::lock_guard<std::recursive_mutex> lock(m_);
element_pointer curr = data_element_;
while (curr != nullptr) {
element_pointer prev = curr->next;
operator delete(reinterpret_cast<void*>(curr));
curr = prev;
}
curr = free_element_;
while (curr != nullptr) {
element_pointer prev = curr->next;
operator delete(reinterpret_cast<void*>(curr));
curr = prev;
}
}
template <typename T, size_t BlockSize, bool ZeroOnDeallocate>
inline typename MemoryPool<T, BlockSize, ZeroOnDeallocate>::pointer
MemoryPool<T, BlockSize, ZeroOnDeallocate>::address(reference x)
const noexcept {
return &x;
}
template <typename T, size_t BlockSize, bool ZeroOnDeallocate>
inline typename MemoryPool<T, BlockSize, ZeroOnDeallocate>::const_pointer
MemoryPool<T, BlockSize, ZeroOnDeallocate>::address(const_reference x)
const noexcept {
return &x;
}
template <typename T, size_t BlockSize, bool ZeroOnDeallocate>
void
MemoryPool<T, BlockSize, ZeroOnDeallocate>::allocateBlock() {
// Allocate space for the new block and store a pointer to the previous one
data_pointer new_block = reinterpret_cast<data_pointer> (operator new(BlockSize));
element_pointer new_ele_pointer = reinterpret_cast<element_pointer>(new_block);
new_ele_pointer->pre = nullptr;
new_ele_pointer->next = nullptr;
if (data_element_) {
data_element_->pre = new_ele_pointer;
}
new_ele_pointer->next = data_element_;
data_element_ = new_ele_pointer;
}
template <typename T, size_t BlockSize, bool ZeroOnDeallocate>
inline typename MemoryPool<T, BlockSize, ZeroOnDeallocate>::pointer
MemoryPool<T, BlockSize, ZeroOnDeallocate>::allocate(size_type n, const_pointer hint) {
std::lock_guard<std::recursive_mutex> lock(m_);
if (free_element_ != nullptr) {
data_pointer body =
reinterpret_cast<data_pointer>(reinterpret_cast<data_pointer>(free_element_) + sizeof(element_type));
size_type bodyPadding = padPointer(body, alignof(element_type));
pointer result = reinterpret_cast<pointer>(reinterpret_cast<data_pointer>(body + bodyPadding));
element_pointer tmp = free_element_;
free_element_ = free_element_->next;
if (free_element_)
free_element_->pre = nullptr;
tmp->next = data_element_;
if (data_element_)
data_element_->pre = tmp;
tmp->pre = nullptr;
data_element_ = tmp;
return result;
}
else {
allocateBlock();
data_pointer body =
reinterpret_cast<data_pointer>(reinterpret_cast<data_pointer>(data_element_) + sizeof(element_type));
size_type bodyPadding = padPointer(body, alignof(element_type));
pointer result = reinterpret_cast<pointer>(reinterpret_cast<data_pointer>(body + bodyPadding));
return result;
}
}
template <typename T, size_t BlockSize, bool ZeroOnDeallocate>
inline void
MemoryPool<T, BlockSize, ZeroOnDeallocate>::deallocate(pointer p, size_type n) {
std::lock_guard<std::recursive_mutex> lock(m_);
if (p != nullptr) {
element_pointer ele_p =
reinterpret_cast<element_pointer>(reinterpret_cast<data_pointer>(p) - sizeof(element_type));
if (ZeroOnDeallocate) {
memset(reinterpret_cast<data_pointer>(p), 0, BlockSize - sizeof(element_type));
}
if (ele_p->pre) {
ele_p->pre->next = ele_p->next;
}
if (ele_p->next) {
ele_p->next->pre = ele_p->pre;
}
if (ele_p->pre == nullptr) {
data_element_ = ele_p->next;
}
ele_p->pre = nullptr;
if (free_element_) {
ele_p->next = free_element_;
free_element_->pre = ele_p;
}
else {
ele_p->next = nullptr;
}
free_element_ = ele_p;
}
}
template <typename T, size_t BlockSize, bool ZeroOnDeallocate>
inline typename MemoryPool<T, BlockSize, ZeroOnDeallocate>::size_type
MemoryPool<T, BlockSize, ZeroOnDeallocate>::max_size()
const noexcept {
size_type maxBlocks = -1 / BlockSize;
return (BlockSize - sizeof(data_pointer)) / sizeof(element_type) * maxBlocks;
}
template <typename T, size_t BlockSize, bool ZeroOnDeallocate>
template <class U, class... Args>
inline void
MemoryPool<T, BlockSize, ZeroOnDeallocate>::construct(U* p, Args&&... args) {
new (p) U(std::forward<Args>(args)...);
}
template <typename T, size_t BlockSize, bool ZeroOnDeallocate>
template <class U>
inline void
MemoryPool<T, BlockSize, ZeroOnDeallocate>::destroy(U* p) {
p->~U();
}
template <typename T, size_t BlockSize, bool ZeroOnDeallocate>
template <class... Args>
inline typename MemoryPool<T, BlockSize, ZeroOnDeallocate>::pointer
MemoryPool<T, BlockSize, ZeroOnDeallocate>::newElement(Args&&... args) {
std::lock_guard<std::recursive_mutex> lock(m_);
pointer result = allocate();
construct<value_type>(result, std::forward<Args>(args)...);
return result;
}
template <typename T, size_t BlockSize, bool ZeroOnDeallocate>
inline void
MemoryPool<T, BlockSize, ZeroOnDeallocate>::deleteElement(pointer p) {
std::lock_guard<std::recursive_mutex> lock(m_);
if (p != nullptr) {
p->~value_type();
deallocate(p);
}
}
}
}
#endif // PPX_BASE_MEMORY_POOL_H_
使用示例:
#include <iostream>
#include <thread>
using namespace std;
class Apple {
public:
Apple() {
id_ = 0;
cout << "Apple()" << endl;
}
Apple(int id) {
id_ = id;
cout << "Apple(" << id_ << ")" << endl;
}
~Apple() {
cout << "~Apple()" << endl;
}
void SetId(int id) {
id_ = id;
}
int GetId() {
return id_;
}
private:
int id_;
};
void ThreadProc(ppx::base::MemoryPool<char> *mp) {
int i = 0;
while (i++ < 100000) {
char* p0 = (char*)mp->allocate();
char* p1 = (char*)mp->allocate();
mp->deallocate(p0);
char* p2 = (char*)mp->allocate();
mp->deallocate(p1);
mp->deallocate(p2);
}
}
int main()
{
ppx::base::MemoryPool<char> mp;
int i = 0;
while (i++ < 100000) {
char* p0 = (char*)mp.allocate();
char* p1 = (char*)mp.allocate();
mp.deallocate(p0);
char* p2 = (char*)mp.allocate();
mp.deallocate(p1);
mp.deallocate(p2);
}
std::thread th0(ThreadProc, &mp);
std::thread th1(ThreadProc, &mp);
std::thread th2(ThreadProc, &mp);
th0.join();
th1.join();
th2.join();
Apple *apple = nullptr;
{
ppx::base::MemoryPool<Apple> mp2;
apple = mp2.newElement(10);
int a = apple->GetId();
apple->SetId(10);
a = apple->GetId();
mp2.deleteElement(apple);
}
apple->SetId(12);
int b = -4 % 4;
int *a = nullptr;
{
ppx::base::MemoryPool<int, 18> mp3;
a = mp3.allocate();
*a = 100;
//mp3.deallocate(a);
int *b = mp3.allocate();
*b = 200;
//mp3.deallocate(b);
mp3.deallocate(a);
mp3.deallocate(b);
int *c = mp3.allocate();
*c = 300;
}
getchar();
return 0;
}
5.5避开这些坑,让内存池更完美
在探索内存池的征途中,也有一些暗藏的 “礁石” 需要留意,稍有不慎,就可能让内存池这叶 “扁舟” 偏离高效运行的航道。
内存泄漏隐患是首当其冲的问题。虽然内存池本身旨在避免传统内存管理中的泄漏风险,但倘若在内存池的实现代码里,对空闲链表的操作有误,比如在回收内存块插入链表时指针赋值出错,导致内存块 “迷失” 在内存荒野,无法被再次分配,日积月累,就如同堤坝出现微小裂缝,最终也可能引发溃堤之灾,程序可用内存被悄然蚕食。
线程同步问题宛如一团乱麻,困扰着多线程环境下的内存池应用。当多个线程高频访问内存池,若互斥锁使用不当,就可能引发死锁。例如线程 A 持有锁 1 并等待锁 2,而线程 B 持有锁 2 又在等待锁 1,二者僵持不下,内存分配与回收流程陷入停滞,整个程序如同被施了定身咒,动弹不得,严重影响运行效率与稳定性。
过度的内存预分配也是个容易踏入的误区。开发者满心想着为程序运行备足 “弹药”,却未精准预估内存需求,申请了一块大得离谱的内存池。这不仅造成程序启动初期内存占用飙升,还可能导致大量内存闲置浪费,其他急需内存的程序或系统组件只能望 “内存” 兴叹,系统整体性能因此失衡,得不偿失。
为躲开这些陷阱,严谨的代码审查与测试是关键护盾。在代码编写阶段,对涉及内存池操作的每一行代码都要反复推敲,确保链表操作、指针赋值等准确无误;借助专业的内存检测工具,如 Valgrind 等,像侦探一样揪出潜在的内存泄漏点。对于多线程同步,运用成熟的线程分析工具排查死锁风险,合理设计锁的获取与释放逻辑,必要时采用更高级的无锁编程技巧化解难题。在规划内存池大小时,结合性能分析数据与实际业务场景,精打细算,用最小的内存开销支撑程序的顺畅运行,让内存池在正确的轨道上全速前进,助力程序性能腾飞。
六、并发内存池项目实战
6.1 项目介绍
我写这个项目呢,主要是为了学习,参考的是tc_malloc,项目设计分为三层结构:
图片
- 第一层是Thread Cache,线程缓存是每个线程独有的,在这里设计的是用于小于64k的内存分配,线程在这里申请不需要加锁,每一个线程都有自己独立的cache,这也就是这个项目并发高效的地方。
- 第二层是Central Cache,在这里是所有线程共享的,它起着承上启下的作用,Thread Cache是按需要从Central Cache中获取对象,它就要起着平衡多个线程按需调度的作用,既可以将内存对象分配给Thread Cache来的每个线程,又可以将线程归还回来的内存进行管理。Central Cache是存在竞争的,所以在这里取内存对象的时候是需要加锁的,但是锁的力度可以控制得很小。
- 第三层是Page Cache,存储的是以页为单位存储及分配的,Central Cache没有内存对象(Span)时,从Page cache分配出一定数量的Page,并切割成定长大小的小块内存,分配给Central Cache。Page Cache会回收Central Cache满足条件的Span(使用计数为0)对象,并且合并相邻的页,组成更大的页,缓解内存碎片的问题。
注:怎么实现每个线程都拥有自己唯一的线程缓存呢?
为了避免加锁带来的效率,在Thread Cache中使用(tls)thread local storage保存每个线程本地的Thread Cache的指针,这样Thread Cache在申请释放内存是不需要锁的。因为每一个线程都拥有了自己唯一的一个全局变量。
TLS分为静态的和动态的:
- 静态的TLS是:直接定义
- 动态的TLS是:调用系统的API去创建的,我们这个项目里面用到的就是静态的TLS
6.2 设计Thread Cache
图片
ThreadCache.h:
#pragma once
#include "Common.h"
class ThreadCache
{
private:
Freelist _freelist[NLISTS];//自由链表
public:
//申请和释放内存对象
void* Allocate(size_t size);
void Deallocate(void* ptr, size_t size);
//从中心缓存获取对象
void* FetchFromCentralCache(size_t index, size_t size);
//释放对象时,链表过长时,回收内存回到中心堆
void ListTooLong(Freelist* list, size_t size);
};
//静态的,不是所有可见
//每个线程有个自己的指针, 用(_declspec (thread)),我们在使用时,每次来都是自己的,就不用加锁了
//每个线程都有自己的tlslist
_declspec (thread) static ThreadCache* tlslist = nullptr;
申请内存:
- 当内存申请size<=64k时在Thread Cache中申请内存,计算size在自由链表中的位置,如果自由链表中有内存对象时,直接从FistList[i]中Pop一下对象,时间复杂度是O(1),且没有锁竞争。
- 当FreeList[i]中没有对象时,则批量从Central Cache中获取一定数量的对象,插入到自由链表并返回一个对象。
释放内存:
- 当释放内存小于64k时将内存释放回Thread Cache,计算size在自由链表中的位置,将对象Push到FreeList[i].
- 当链表的长度过长,也就是超过一次向中心缓存分配的内存块数目时则回收一部分内存对象到Central Cache。
6.3 对齐大小的设计(对齐规则)
//专门用来计算大小位置的类
class SizeClass
{
public:
//获取Freelist的位置
inline static size_t _Index(size_t size, size_t align)
{
size_t alignnum = 1 << align; //库里实现的方法
return ((size + alignnum - 1) >> align) - 1;
}
inline static size_t _Roundup(size_t size, size_t align)
{
size_t alignnum = 1 << align;
return (size + alignnum - 1)&~(alignnum - 1);
}
public:
// 控制在12%左右的内碎片浪费
// [1,128] 8byte对齐 freelist[0,16)
// [129,1024] 16byte对齐 freelist[16,72)
// [1025,8*1024] 128byte对齐 freelist[72,128)
// [8*1024+1,64*1024] 1024byte对齐 freelist[128,184)
inline static size_t Index(size_t size)
{
assert(size <= MAX_BYTES);
// 每个区间有多少个链
static int group_array[4] = { 16, 56, 56, 56 };
if (size <= 128)
{
return _Index(size, 3);
}
else if (size <= 1024)
{
return _Index(size - 128, 4) + group_array[0];
}
else if (size <= 8192)
{
return _Index(size - 1024, 7) + group_array[0] + group_array[1];
}
else//if (size <= 65536)
{
return _Index(size - 8 * 1024, 10) + group_array[0] + group_array[1] + group_array[2];
}
}
// 对齐大小计算,向上取整
static inline size_t Roundup(size_t bytes)
{
assert(bytes <= MAX_BYTES);
if (bytes <= 128){
return _Roundup(bytes, 3);
}
else if (bytes <= 1024){
return _Roundup(bytes, 4);
}
else if (bytes <= 8192){
return _Roundup(bytes, 7);
}
else {//if (bytes <= 65536){
return _Roundup(bytes, 10);
}
}
//动态计算从中心缓存分配多少个内存对象到ThreadCache中
static size_t NumMoveSize(size_t size)
{
if (size == 0)
return 0;
int num = (int)(MAX_BYTES / size);
if (num < 2)
num = 2;
if (num > 512)
num = 512;
return num;
}
// 根据size计算中心缓存要从页缓存获取多大的span对象
static size_t NumMovePage(size_t size)
{
size_t num = NumMoveSize(size);
size_t npage = num*size;
npage >>= PAGE_SHIFT;
if (npage == 0)
npage = 1;
return npage;
}
};
6.4 设计Thread Cache
- Central Cache本质是由一个哈希映射的Span对象自由双向链表构成
- 为了保证全局只有唯一的Central Cache,这个类被可以设计成了单例模式
- 单例模式采用饿汉模式,避免高并发下资源的竞争
图片
CentralCache.h:
#pragma once
#include "Common.h"
//上面的ThreadCache里面没有的话,要从中心获取
/*
进行资源的均衡,对于ThreadCache的某个资源过剩的时候,可以回收ThreadCache内部的的内存
从而可以分配给其他的ThreadCache
只有一个中心缓存,对于所有的线程来获取内存的时候都应该是一个中心缓存
所以对于中心缓存可以使用单例模式来进行创建中心缓存的类
对于中心缓存来说要加锁
*/
//设计成单例模式
class CentralCache
{
public:
static CentralCache* Getinstence()
{
return &_inst;
}
//从page cache获取一个span
Span* GetOneSpan(SpanList& spanlist, size_t byte_size);
//从中心缓存获取一定数量的对象给threa cache
size_t FetchRangeObj(void*& start, void*& end, size_t n, size_t byte_size);
//将一定数量的对象释放给span跨度
void ReleaseListToSpans(void* start, size_t size);
private:
SpanList _spanlist[NLISTS];
private:
CentralCache(){}//声明不实现,防止默认构造,自己创建
CentralCache(CentralCache&) = delete;
static CentralCache _inst;
};
申请内存:
- 当Thread Cache中没有内存时,就会批量向Central Cache申请一些内存对象,Central Cache也有一个哈希映射的freelist,freelist中挂着span,从span中取出对象给Thread Cache,这个过程是需要加锁的。
- Central Cache中没有非空的span时,则将空的span链在一起,向Page Cache申请一个span对象,span对象中是一些以页为单位的内存,切成需要的内存大小,并链接起来,挂到span中。
- Central Cache的span中有一个_usecount,分配一个对象给Thread Cache,就++_usecount。
释放内存:
- 当Thread Cache过长或者线程销毁,则会将内存释放回Central Cache中的,释放回来时- -_usecount。
- 当_usecount减到0时则表示所有对象都回到了span,则将Span释放回Page Cache,Page Cache中会对前后相邻的空闲页进行合并。
特别关心:什么是span?一个span是由多个页组成的一个span对象。一页大小是4k。
//Span是一个跨度,既可以分配内存出去,也是负责将内存回收回来到PageCache合并
//是一链式结构,定义为结构体就行,避免需要很多的友元
struct Span
{
PageID _pageid = 0;//页号
size_t _npage = 0;//页数
Span* _prev = nullptr;
Span* _next = nullptr;
void* _list = nullptr;//链接对象的自由链表,后面有对象就不为空,没有对象就是空
size_t _objsize = 0;//对象的大小
size_t _usecount = 0;//对象使用计数,
};
特别关心:关于spanlist,设计为一个双向链表,插入删除效率较高。
//和上面的Freelist一样,各个接口自己实现,双向带头循环的Span链表
class SpanList
{
public:
Span* _head;
std::mutex _mutex;
public:
SpanList()
{
_head = new Span;
_head->_next = _head;
_head->_prev = _head;
}
~SpanList()//释放链表的每个节点
{
Span * cur = _head->_next;
while (cur != _head)
{
Span* next = cur->_next;
delete cur;
cur = next;
}
delete _head;
_head = nullptr;
}
//防止拷贝构造和赋值构造,将其封死,没有拷贝的必要,不然就自己会实现浅拷贝
SpanList(const SpanList&) = delete;
SpanList& operator=(const SpanList&) = delete;
//左闭右开
Span* Begin()//返回的一个数据的指针
{
return _head->_next;
}
Span* End()//最后一个的下一个指针
{
return _head;
}
bool Empty()
{
return _head->_next == _head;
}
//在pos位置的前面插入一个newspan
void Insert(Span* cur, Span* newspan)
{
Span* prev = cur->_prev;
//prev newspan cur
prev->_next = newspan;
newspan->_next = cur;
newspan->_prev = prev;
cur->_prev = newspan;
}
//删除pos位置的节点
void Erase(Span* cur)//此处只是单纯的把pos拿出来,并没有释放掉,后面还有用处
{
Span* prev = cur->_prev;
Span* next = cur->_next;
prev->_next = next;
next->_prev = prev;
}
//尾插
void PushBack(Span* newspan)
{
Insert(End(), newspan);
}
//头插
void PushFront(Span* newspan)
{
Insert(Begin(), newspan);
}
//尾删
Span* PopBack()//实际是将尾部位置的节点拿出来
{
Span* span = _head->_prev;
Erase(span);
return span;
}
//头删
Span* PopFront()//实际是将头部位置节点拿出来
{
Span* span = _head->_next;
Erase(span);
return span;
}
void Lock()
{
_mutex.lock();
}
void Unlock()
{
_mutex.unlock();
}
};
特别关心:怎么才能将Thread Cache中的内存对象还给它原来的span呢?
答:可以在Page Cache中维护一个页号到span的映射,当Span Cache给Central Cache分配一个span时,将这个映射更新到unordered_map中去,在Thread Cache还给Central Cache时,可以查这个unordered_map找到对应的span。
6.5 设计Page Cache
- Page cache是一个以页为单位的span自由链表。
- 为了保证全局只有唯一的Page cache,这个类可以被设计成了单例模式。
- 本单例模式采用饿汉模式。
图片
PageCache.h
#pragma once
#include "Common.h"
//对于Page Cache也要设置为单例,对于Central Cache获取span的时候
//每次都是从同一个page数组中获取span
//单例模式
class PageCache
{
public:
static PageCache* GetInstence()
{
return &_inst;
}
Span* AllocBigPageObj(size_t size);
void FreeBigPageObj(void* ptr, Span* span);
Span* _NewSpan(size_t n);
Span* NewSpan(size_t n);//获取的是以页为单位
//获取从对象到span的映射
Span* MapObjectToSpan(void* obj);
//释放空间span回到PageCache,并合并相邻的span
void ReleaseSpanToPageCache(Span* span);
private:
SpanList _spanlist[NPAGES];
//std::map<PageID, Span*> _idspanmap;
std::unordered_map<PageID, Span*> _idspanmap;
std::mutex _mutex;
private:
PageCache(){}
PageCache(const PageCache&) = delete;
static PageCache _inst;
};
申请内存:
- 当Central Cache向page cache申请内存时,Page Cache先检查对应位置有没有span,如果没有则向更大页寻找一个span,如果找到则分裂成两个。比如:申请的是4page,4page后面没有挂span,则向后面寻找更大的span,假设在10page位置找到一个span,则将10page span分裂为一个4page span和一个6page span。
- 如果找到128 page都没有合适的span,则向系统使用mmap、brk或者是VirtualAlloc等方式申请128page span挂在自由链表中,再重复1中的过程。
释放内存:如果Central Cache释放回一个span,则依次寻找span的前后_pageid的span,看是否可以合并,如果合并继续向前寻找。这样就可以将切小的内存合并收缩成大的span,减少内存碎片。
6.6 项目不足
项目的独立性不足:
- 不足:当前实现的项目中我们并没有完全脱离malloc,比如在内存池自身数据结构的管理中,如SpanList中的span等结构,我们还是使用的new Span这样的操作,new的底层使用的是malloc,所以还不足以替换malloc,因为们本身没有完全脱离它。
- 解决方案:项目中增加一个定长的ObjectPool的对象池,对象池的内存直接使用brk、VirarulAlloc等向系统申请,new Span替换成对象池申请内存。这样就完全脱离的malloc,就可以替换掉malloc。
平台及兼容性:
- linux等系统下面,需要将VirtualAlloc替换为brk等。
- x64系统下面,当前的实现支持不足。比如:id查找Span得到的映射,我们当前使用的是map<id,
Span*>。在64位系统下面,这个数据结构在性能和内存等方面都是撑不住。需要改进后基数树。 - 具体参考:Linux内核数据结构:基数树Radix Tree