破解内存瓶颈:如何通过内存池优化资源利用
想象一下,你经营着一家热闹的餐厅 ,用餐高峰期时,顾客们来来往往,服务员需要不断地为新顾客安排座位,收拾离开顾客的餐桌。如果每次有新顾客到来,都要临时去找可用的桌椅,不仅效率低下,还可能因为频繁挪动和寻找造成餐厅的混乱,桌椅摆放杂乱无章。但要是餐厅提前准备了一个 “桌椅储备区”,当有顾客需要时,直接从储备区调配桌椅,顾客离开后再将桌椅放回储备区,这样是不是就能大大提高服务效率,让餐厅运营更加顺畅呢?
在计算机的世界里,内存就如同餐厅里的桌椅,是程序运行不可或缺的资源 。程序在运行过程中,会频繁地进行内存的申请(就像为新顾客安排座位)和释放(收拾离开顾客的餐桌)操作。传统的内存分配方式,比如使用malloc、new等函数,每次申请内存时,都要与操作系统进行交互,这就好比每次为新顾客找桌椅都要去仓库重新搬一套,过程繁琐且耗时。而且,频繁地申请和释放内存,会导致内存空间变得碎片化,就像餐厅里的桌椅被随意摆放,难以找到连续的大片可用空间,最终影响程序的性能 。
为了解决这些问题,内存池(Memory Pool)应运而生,它就像是餐厅的 “桌椅储备区” 。内存池在程序启动时,一次性向操作系统申请一大块内存,然后将这块内存划分成多个小块进行管理。当程序需要内存时,直接从内存池中获取,而不是每次都向操作系统请求;当程序释放内存时,也不是直接还给操作系统,而是归还到内存池,以便后续再次使用。这样一来,大大减少了与操作系统的交互次数,提高了内存分配和释放的效率,同时也有效减少了内存碎片的产生,让程序运行得更加高效、稳定 。
一、内存池简介
1.1池化技术
池是在计算技术中经常使用的一种设计模式,其内涵在于:将程序中需要经常使用的核心资源先申请出来,放到一个池内,由程序自管理,这样可以提高资源的利用率,也可以保证本程序占有的资源数量,经常使用的池化技术包括内存池,线程池,和连接池等,其中尤以内存池和线程池使用最多
1.2内存池
内存池是一种管理内存分配和释放的技术。它通过预先分配一块连续的内存空间,然后将其划分成多个固定大小的小块,称为内存块或内存页。当需要分配内存时,从内存池中取出一个可用的内存块,并在使用完成后将其标记为已用。而不是频繁地向操作系统请求动态分配和释放内存。
通过使用内存池,可以减少因频繁进行动态内存分配和释放而导致的性能开销和碎片化问题。它特别适用于那些需要频繁申请和释放相同大小的对象的场景,如网络服务器、数据库系统等。通过优化了对象分配过程,并提供更高效的资源利用率,从而提升程序的性能和可伸缩性。内存池通常用于高性能的服务器程序、嵌入式系统和实时系统等场景中。
mempool_t是Linux内核提供的一种内存池实现,包含于<linux/mempool.h>头文件中。mempool_t提供了一种简单、高效的内存池管理方法,可以用于在内核模块中管理预分配的内存块。mempool_t支持对内存块的分配、回收和管理,并提供了多种内存池算法以适应不同的应用场景。使用mempool_t可以显著降低内存分配和释放的成本,避免内存碎片的产生,提高内核模块的性能和可靠性。
在 Linux 内核中,可以使用 kmem_cache_create()、kmem_cache_alloc() 和 kmem_cache_free() 函数来创建和使用内存池。以下是它们的详细说明:
(1)kmem_cache_create(): 用于创建内存池。
struct kmem_cache *kmem_cache_create(const char *name, size_t size, size_t align,
unsigned long flags, void (*ctor)(void *));
参数说明:
-
name: 内存池的名称。
-
size: 每个对象的大小。
-
align: 对齐方式(通常设置为ARCH_KMALLOC_MINALIGN)。
-
flags: 标志位,如GFP_KERNEL、GFP_ATOMIC等。
-
ctor: 构造函数指针,用于初始化新分配的对象。
(2)kmem_cache_alloc(): 用于从内存池中分配内存块。
void *kmem_cache_alloc(struct kmem_cache *cache, gfp_t flags);
参数说明:
-
cache: 内存池结构体指针,通过 kmem_cache_create() 创建得到。
-
flags: 分配内存时的标志位。
(3)kmem_cache_free(): 用于将已分配的内存块释放回内存池中。
void kmem_cache_free(struct kmem_cache *cache, void *obj);
参数说明:
-
cache: 内存池结构体指针,通过 kmem_cache_create() 创建得到。
-
obj: 要释放的内存块指针。
(4)kmem_cache_init(): 在模块加载时初始化内存池。
int kmem_cache_init(void);
(5)kmem_cache_destroy(): 在模块卸载时销毁内存池。
void kmem_cache_destroy(struct kmem_cache *cache);
二、为什么吸引内存池?
2.1 内存碎片问题
造成堆利用率很低的一个主要原因就是内存碎片化。如果有未使用的存储器,但是这块存储器不能用来满足分配的请求,这时候就会产生内存碎片化问题。内存碎片化分为内部碎片和外部碎片。
内部碎片:是指一个已分配的块比有效载荷大时发生的。(假设以前分配了10个大小的字节,现在只用了5个字节,则剩下的5个字节就会内碎片)。内部碎片的大小就是已经分配的块的大小和他们的有效载荷之差的和。因此内部碎片取决于以前请求内存的模式和分配器实现(对齐的规则)的模式。
外部碎片:假设系统依次分配了16byte、8byte、16byte、4byte,还剩余8byte未分配。这时要分配一个24byte的空间,操作系统回收了一个上面的两个16byte,总的剩余空间有40byte,但是却不能分配出一个连续24byte的空间,这就是外碎片问题。
2.2申请效率问题
例如:我们上学家里给生活费一样,假设一学期的生活费是6000块。
-
方式1:开学时6000块直接给你,自己保管,自己分配如何花。
-
方式2:每次要花钱时,联系父母,父母转钱。
同样是6000块钱,第一种方式的效率肯定更高,因为第二种方式跟父母的沟通交互成本太高了。
同样的道理,程序就像是上学的我们,操作系统就像父母,频繁申请内存的场景下,每次需要内存,都像系统申请效率必然有影响。
2.3常见内存池实现方案
-
(1)固定大小缓冲池:固定大小缓冲池适用于频繁分配和释放固定大小对象的情况。
-
(2)dlmalloc:dlmalloc 是一个内存分配器,由Doug Lea从1987年开始编写,目前最新版本为2.8.3,由于其高效率等特点被广泛使用和研究。
-
(3) SGI STL内存分配器:SGI STL allocator 是目前设计最优秀的 C++ 内存分配器之一,其内部free_list[16] 数组负责管理从 8 bytes到128 bytes不同大小的内存块( chunk ),每一个内存块都由连续的固定大小( fixed size block )的很多 chunk 组成,并用指针链表连接。
-
(4)Loki小对象分配器:Loki 分配器使用vector管理数组,可以指定 fixed size block 的大小。free blocks分布在一个连续的大内存块中,free chunks 可以根据使用情况自动增长和减少合适的数目,避免内存分配得过多或者过少。
-
(5)Boost object_pool:可以根据用户具体应用类的大小来分配内存块,通过维护一个free nodes的链表来管理。可以自动增加nodes块,初始32个nodes,每次增加都以两倍数向system heap要内存块。object_pool 管理的内存块需要在其对象销毁的时候才返还给 system heap 。
-
(6)ACE_Cached_Allocator 和 ACE_Free_List:ACE 框架中包含一个可以维护固定大小的内存块的分配器,通过在 ACE_Cached_Allocator 中定义Free_list 链表来管理一个连续的大内存块,内存块中包含多个固定大小的未使用内存区块( free chunk),同时使用ACE_unbounded_Set维护已使用的chuncks 。
-
(7)TCMalloc:Google开源项目gperftools提供了内存池实现方案。TCMalloc替换了系统的malloc,更加底层优化,性能更好。
三、内存池核心原理
初始化阶段:在程序启动时,内存池首先要进行初始化 。这就好比开餐厅前要先规划好桌椅储备区的大小和布局。内存池会根据程序的需求,向操作系统申请一块足够大的连续内存空间 。申请到内存后,内存池会根据预先设定好的规则,把这块大内存分割成多个大小相等或不等的小内存块 。这些小内存块会被组织成特定的数据结构,比如链表、位图等,方便后续的管理和分配 。例如,我们可以用链表将这些小内存块串联起来,每个内存块都包含指向下一个内存块的指针,这样就形成了一个内存块链表,就像把仓库里的小格子用通道连接起来,方便取用 。
内存分配阶段:当程序需要分配内存时,会向内存池发出请求 。内存池会根据自身的分配策略,从已有的空闲内存块中挑选一个合适的分配给程序 。如果是固定大小内存池,每个内存块大小固定,分配过程就比较简单,直接从链表头部取出一个空闲内存块即可 ,就像从储备区最前面拿一套桌椅给顾客。如果是可变大小内存池,内存池则需要根据请求的内存大小,在内存块链表中寻找一个大小合适的空闲内存块 ,如果没有正好合适的,可能会选择一个稍大的内存块,然后将其分割成所需大小和一个新的空闲内存块 ,再把所需大小的内存块分配出去 。在这个过程中,内存池还需要更新相关的数据结构,标记该内存块已被使用 ,比如修改链表指针或者位图中的标志位 。
内存释放阶段:当程序不再需要某个内存块时,会将其释放回内存池 。内存池接收到释放的内存块后,会将其重新标记为空闲状态,并将其放回内存块链表或相应的数据结构中 ,以便后续再次分配 。如果内存池采用的是链表结构,释放的内存块会被插入到链表的合适位置,可能是头部,也可能是根据某种规则插入到链表中间 ,就像把顾客用完的桌椅放回储备区合适的位置 。对于可变大小内存池,如果释放的内存块与相邻的空闲内存块相邻,内存池还会将它们合并成一个更大的空闲内存块 ,以减少内存碎片 ,就像把相邻的空桌椅区域合并成一个更大的空区域 。
下面通过一段简单的 C++ 代码示例,来更直观地感受内存池的工作过程:
#include <iostream>
#include <cstdlib>
// 定义内存块结构体
struct MemoryBlock {
size_t size; // 内存块大小
bool isFree; // 是否空闲
MemoryBlock* next; // 指向下一个内存块的指针
};
// 定义内存池类
class MemoryPool {
public:
MemoryPool(size_t poolSize, size_t blockSize) : poolSize(poolSize), blockSize(blockSize) {
// 申请内存池空间
pool = static_cast<MemoryBlock*>(std::malloc(poolSize));
if (pool == nullptr) {
std::cerr << "内存池初始化失败" << std::endl;
return;
}
// 初始化内存块链表
MemoryBlock* current = pool;
for (size_t i = 0; i < poolSize / blockSize - 1; ++i) {
current->size = blockSize;
current->isFree = true;
current->next = current + 1;
current = current->next;
}
current->size = blockSize;
current->isFree = true;
current->next = nullptr;
freeList = pool; // 空闲链表头指针指向第一个内存块
}
~MemoryPool() {
std::free(pool);
}
// 分配内存
void* allocate() {
if (freeList == nullptr) {
std::cerr << "内存池已无空闲内存块" << std::endl;
return nullptr;
}
MemoryBlock* allocatedBlock = freeList;
freeList = freeList->next;
allocatedBlock->isFree = false;
return allocatedBlock;
}
// 释放内存
void deallocate(void* block) {
if (block == nullptr) {
return;
}
MemoryBlock* freedBlock = static_cast<MemoryBlock*>(block);
freedBlock->isFree = true;
freedBlock->next = freeList;
freeList = freedBlock;
}
private:
MemoryBlock* pool; // 内存池起始地址
MemoryBlock* freeList; // 空闲内存块链表头指针
size_t poolSize; // 内存池大小
size_t blockSize; // 每个内存块大小
};
int main() {
MemoryPool pool(1024, 64); // 创建一个大小为1024字节,每个内存块为64字节的内存池
void* block1 = pool.allocate(); // 分配内存块
void* block2 = pool.allocate();
pool.deallocate(block1); // 释放内存块
void* block3 = pool.allocate();
return 0;
}
在这段代码中,MemoryPool 类实现了一个简单的固定大小内存池 。在构造函数中,它向系统申请了一块大小为 poolSize 的内存,并将其划分为多个大小为 blockSize 的内存块,通过链表将这些内存块连接起来,构建了空闲内存块链表 。allocate 方法从空闲链表中取出一个内存块并标记为已使用,deallocate 方法则将释放的内存块重新加入空闲链表 。通过这个示例,我们可以清晰地看到内存池的初始化、分配和释放过程 。
四、内存池设计:从理论到实践
4.1为什么要使用内存池
- 提高性能:内存池可以预先分配一块连续的物理内存空间,并将其划分为多个大小相等的块。这样,在后续需要分配内存时,可以直接从缓存中获取已经准备好的内存对象,而不需要每次都进行物理内存分配操作,从而提高了内存分配的效率。
- 减少碎片化:频繁地进行小块内存分配和释放容易导致堆内存碎片化,使得可用的连续内存空间变少。而使用内存池可以减少碎片化问题,因为它在预先分配阶段就已经将一大块连续内存划分成固定大小的小块,并且通过循环利用来管理这些小块。
- 简化管理:使用内存池可以简化对于小块内存对象的管理。它提供了一个数据结构来缓冲已经分配好的内存对象,并跟踪哪些是可用的、哪些是被占用的。这样,在应用程序中只需调用相应的函数来申请和释放内存对象即可,无需手动进行复杂的管理。
- 控制资源消耗:由于内存池提前申请一定数量的内存块,并按需分配,可以有效地控制对系统资源的消耗。这对于一些具有严格资源限制的环境或嵌入式系统特别重要。
4.2工作原理
-
初始化:在内存池被创建时,需要预先分配一块连续的物理内存空间。这个空间可以是从操作系统申请的大块内存,也可以是来自其他资源的预留空间。
-
划分内存块:将初始化得到的内存空间划分为大小相等的小块(也称为对象或节点)。每个小块都有固定大小,并且可以容纳一个特定类型的数据对象。
-
管理空闲链表:使用一个数据结构(通常是链表或数组)来管理已经分配和未分配的小块。初始化时,所有小块都会被链接成一个空闲链表。每当有代码请求申请内存时,会从空闲链表中取出一个空闲的小块供使用。
-
分配内存:当有代码请求申请内存时,内存池会从空闲链表中获取一个可用的小块,并将其标记为已分配状态。然后返回该小块给调用方使用。
-
释放内存:当不再需要某个已经分配的小块时,调用方通过释放函数将其返回给内存池。此时,该小块会被标记为空闲状态,并重新加入到空闲链表中以供下次使用。
-
扩展内存池(可选):在某些情况下,如果内存池中的空闲小块不够用了,可以考虑扩展内存池的大小。这可能需要重新分配更多的物理内存空间,并将其划分为新的小块。
内存池的主要优势在于可以避免动态分配内存时产生的内存碎片,同时也避免了重复调用内存分配器的开销。由于内存块是预先分配的,因此内存池的内存分配速度相对较快。
在Linux内核中,mempool_t通过使用kmem_cache_t来实现内存池。kmem_cache_t是一种内存高速缓存器,可以用于高效地分配预定义大小的内存块。mempool_t在初始化时会创建一个kmem_cache_t对象,并分配一定数量的内存块。当需要分配内存时,mempool_t从kmem_cache_t中获取内存块并返回。当不需要使用内存块时,将其返回到kmem_cache_t中。
4.3内存池的演变
(1)最简单的内存分配器
做一个链表指向空闲内存,分配就是取出一块来,改写链表,返回,释放就是放回到链表里面,并做好归并。注意做好标记和保护,避免二次释放,还可以花点力气在如何查找最适合大小的内存快的搜索上,减少内存碎片,有空你了还可以把链表换成伙伴算法。
-
优点: 实现简单
-
缺点: 分配时搜索合适的内存块效率低,释放回归内存后归并消耗大,实际中不实用
-
。
(2)定长内存分配器
即实现一个 FreeList,每个 FreeList 用于分配固定大小的内存块,比如用于分配 32字节对象的固定内存分配器,之类的。每个固定内存分配器里面有两个链表,OpenList 用于存储未分配的空闲对象,CloseList用于存储已分配的内存对象,那么所谓的分配就是从 OpenList 中取出一个对象放到 CloseList 里并且返回给用户,释放又是从 CloseList 移回到 OpenList。分配时如果不够,那么就需要增长 OpenList:申请一个大一点的内存块,切割成比如 64 个相同大小的对象添加到 OpenList中。这个固定内存分配器回收的时候,统一把先前向系统申请的内存块全部还给系统。
-
优点:简单粗暴,分配和释放的效率高,解决实际中特定场景下的问题有效。
-
缺点:功能单一,只能解决定长的内存需求,另外占着内存没有释放。
(3)哈希映射的FreeList 池
在定长分配器的基础上,按照不同对象大小(8,16,32,64,128,256,512,1k…64K),构造十多个固定内存分配器,分配内存时根据要申请内存大小进行对齐然后查H表,决定到底由哪个分配器负责,分配后要在内存头部的 header 处写上 cookie,表示由该块内存哪一个分配器分配的,这样释放时候你才能正确归还。如果大于64K,则直接用系统的 malloc作为分配,如此以浪费内存为代价你得到了一个分配时间近似O(1)的内存分配器。这种内存池的缺点是假设某个 FreeList 如果高峰期占用了大量内存即使后面不用,也无法支援到其他内存不够的 FreeList,达不到分配均衡的效果。
-
优点:这个本质是定长内存池的改进,分配和释放的效率高。可以解决一定长度内的问题。
-
缺点:存在内碎片的问题,且将一块大内存切小以后,申请大内存无法使用。多线程并发场景下,锁竞争激烈,效率降低。
-
范例:sgi stl 六大组件中的空间配置器就是这种设计实现的。
(4)了解malloc底层原理
malloc优点:使用自由链表的数组,提高分配释放效率;减少内存碎片,可以合并空闲的内存
malloc缺点:为了维护隐式/显示链表需要维护一些信息,空间利用率不高;在多线程的情况下,会出现线程安全的问题,如果以加锁的方式解决,会大大降低效率。
4.4内存池框架
现行内存池框架主要有以下三种:
-
(1)伙伴算法:伙伴算法是把整块内存分成小块的做法,类似现实生活中的找零钱的做法,将内存分解成2的整数次幂的大小。伙伴算法存在一个问题,回收内存需要两个空闲空间连续且大小一致,所以不适合以字节为单位分配内存的情况,适用于以页为单位分配内存的情况。
-
(2)slab机制:slab也是将内存分成2的整数次幂的大小,与伙伴算法的区别是slab提前分配好,而不是分配时再拆分,没有就向上“借位”。slab适用于小空间分配的情况。tcmalloc、jemalloc是slab的变种。
-
(3)粗放型设计:先给每个业务或连接分配一页空间,当连接断开或业务结束时释放,这也是“粗放”的原因。这种方法适用于特定业务场景。
1)kmem_cache 内存池实现
#include <linux/slab.h>
struct my_struct {
int data;
};
struct kmem_cache *my_cache;
void init_my_pool(void) {
my_cache = kmem_cache_create("my_pool", sizeof(struct my_struct), 0, 0, NULL);
}
void destroy_my_pool(void) {
kmem_cache_destroy(my_cache);
}
struct my_struct *alloc_from_my_pool(void) {
return kmem_cache_alloc(my_cache, GFP_KERNEL);
}
void free_to_my_pool(struct my_struct *ptr) {
kmem_cache_free(my_cache, ptr);
}
2)slab内存池实现:
#include <linux/slab.h>
struct my_struct {
int data;
};
struct kmem_cache *my_slab;
void init_my_pool(void) {
my_slab = kmem_cache_create("my_pool", sizeof(struct my_struct), 0, SLAB_HWCACHE_ALIGN, NULL);
}
void destroy_my_pool(void) {
kmem_cache_destroy(my_slab);
}
struct my_struct *alloc_from_my_pool(void) {
return kmalloc(sizeof(struct my_struct), GFP_KERNEL);
}
void free_to_my_pool(struct my_struct *ptr) {
kfree(ptr);
}
3)slub内存池实现:
#include <linux/slab.h>
struct my_struct {
int data;
};
struct kmem_cache *my_slub;
void init_my_pool(void) {
my_slub = KMEM_CACHE(my_struct, SLUB_PANIC);
}
void destroy_my_pool(void) {
kmem_cache_destroy(my_slub);
}
struct my_struct *alloc_from_my_pool(void) {
return kmem_cache_alloc(my_slub, GFP_KERNEL);
}
void free_to_my_pool(struct my_struct *ptr) {
kmem_cache_free(my_slub, ptr);
}
这些是粗放型设计的示例代码,可根据具体需求进行调整。在使用这些内存池时,请确保正确地初始化和销毁内存池,并使用适当的函数来分配和释放内存块。请注意,以上代码仅为示例,实际使用时应根据需要进行适当的错误处理、边界检查等操作。
五、并发内存池
计划实现一个内存池管理的类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;
}
六、手把手教你实现内存池
6.1前期准备要做好
在实现内存池之前,我们需要先明确所需的工具和技术 。编程语言方面,C 和 C++ 是常见的选择,它们提供了对内存的直接操作能力,能够很好地满足内存池实现的需求 。如果你对 C 语言比较熟悉,那么可以使用 C 语言来实现内存池,其简洁高效的特性适合对性能要求较高的场景 。如果你更倾向于面向对象的编程风格,C++ 则是一个不错的选择,它的类和模板机制可以让代码更加模块化和通用 。
数据结构知识也是必不可少的 。链表、数组、哈希表等数据结构在内存池的实现中都有着广泛的应用 。比如,我们可以用链表来管理空闲内存块,每个内存块作为链表的一个节点,这样在分配和释放内存时,只需要操作链表的指针,效率较高 。数组则可以用于存储内存块的相关信息,如内存块的大小、状态等 。哈希表可以用于快速查找特定大小的内存块,提高内存分配的速度 。
开发环境的选择也很重要 。常用的开发工具如 Visual Studio(Windows 平台)、GCC(Linux 平台)等都提供了丰富的功能和调试工具,能够帮助我们高效地开发和调试内存池代码 。在 Windows 平台上,使用 Visual Studio 可以方便地进行代码编辑、编译和调试,它的集成开发环境(IDE)提供了智能代码提示、语法检查、断点调试等功能,大大提高了开发效率 。在Linux平台上,GCC是一款强大的编译器,配合 GDB 调试工具,可以对内存池代码进行深入的调试和分析 。
6.2代码实现步步为营
下面,我们以 C++ 为例,逐步实现一个简单的固定大小内存池 。
定义内存块和内存池结构体:首先,我们需要定义内存块和内存池的结构体 。内存块结构体用于表示每个内存块的信息,包括内存块的大小、是否空闲以及指向下一个内存块的指针 。内存池结构体则包含内存池的大小、每个内存块的大小、空闲内存块链表的头指针等信息 。
#include <iostream>
#include <cstdlib>
// 定义内存块结构体
struct MemoryBlock {
size_t size; // 内存块大小
bool isFree; // 是否空闲
MemoryBlock* next; // 指向下一个内存块的指针
};
// 定义内存池类
class MemoryPool {
public:
MemoryPool(size_t poolSize, size_t blockSize) : poolSize(poolSize), blockSize(blockSize) {
// 申请内存池空间
pool = static_cast<MemoryBlock*>(std::malloc(poolSize));
if (pool == nullptr) {
std::cerr << "内存池初始化失败" << std::endl;
return;
}
// 初始化内存块链表
MemoryBlock* current = pool;
for (size_t i = 0; i < poolSize / blockSize - 1; ++i) {
current->size = blockSize;
current->isFree = true;
current->next = current + 1;
current = current->next;
}
current->size = blockSize;
current->isFree = true;
current->next = nullptr;
freeList = pool; // 空闲链表头指针指向第一个内存块
}
~MemoryPool() {
std::free(pool);
}
// 分配内存
void* allocate() {
if (freeList == nullptr) {
std::cerr << "内存池已无空闲内存块" << std::endl;
return nullptr;
}
MemoryBlock* allocatedBlock = freeList;
freeList = freeList->next;
allocatedBlock->isFree = false;
return allocatedBlock;
}
// 释放内存
void deallocate(void* block) {
if (block == nullptr) {
return;
}
MemoryBlock* freedBlock = static_cast<MemoryBlock*>(block);
freedBlock->isFree = true;
freedBlock->next = freeList;
freeList = freedBlock;
}
private:
MemoryBlock* pool; // 内存池起始地址
MemoryBlock* freeList; // 空闲内存块链表头指针
size_t poolSize; // 内存池大小
size_t blockSize; // 每个内存块大小
};
初始化内存池:在内存池的构造函数中,我们向系统申请一块大小为poolSize的内存,并将其划分为多个大小为blockSize的内存块 。然后,将这些内存块通过链表连接起来,构建空闲内存块链表 。
MemoryPool::MemoryPool(size_t poolSize, size_t blockSize) : poolSize(poolSize), blockSize(blockSize) {
// 申请内存池空间
pool = static_cast<MemoryBlock*>(std::malloc(poolSize));
if (pool == nullptr) {
std::cerr << "内存池初始化失败" << std::endl;
return;
}
// 初始化内存块链表
MemoryBlock* current = pool;
for (size_t i = 0; i < poolSize / blockSize - 1; ++i) {
current->size = blockSize;
current->isFree = true;
current->next = current + 1;
current = current->next;
}
current->size = blockSize;
current->isFree = true;
current->next = nullptr;
freeList = pool; // 空闲链表头指针指向第一个内存块
}
分配内存:allocate方法用于从内存池中分配内存 。它首先检查空闲链表是否为空,如果为空,则表示内存池已无空闲内存块,返回nullptr 。否则,从空闲链表头部取出一个内存块,将其标记为已使用,并返回该内存块的指针 。
void* MemoryPool::allocate() {
if (freeList == nullptr) {
std::cerr << "内存池已无空闲内存块" << std::endl;
return nullptr;
}
MemoryBlock* allocatedBlock = freeList;
freeList = freeList->next;
allocatedBlock->isFree = false;
return allocatedBlock;
}
释放内存:deallocate方法用于将释放的内存块归还到内存池 。它首先检查传入的指针是否为nullptr,如果是则直接返回 。然后,将释放的内存块标记为空闲,并将其插入到空闲链表的头部 。
void MemoryPool::deallocate(void* block) {
if (block == nullptr) {
return;
}
MemoryBlock* freedBlock = static_cast<MemoryBlock*>(block);
freedBlock->isFree = true;
freedBlock->next = freeList;
freeList = freedBlock;
}
6.3调试优化不能少
在实现内存池后,调试和优化是确保其性能和正确性的关键步骤 。调试内存池代码时,可以使用调试工具,如 GDB(Linux 平台)或 Visual Studio 的调试器(Windows 平台) 。通过设置断点,可以在代码执行到特定位置时暂停,查看变量的值、内存状态等信息,帮助我们找出潜在的问题 。例如,在分配和释放内存的函数中设置断点,可以观察内存块链表的变化,检查是否存在内存泄漏或双重释放等问题 。
添加日志信息也是一种有效的调试方法 。在关键的代码位置,如内存分配、释放和内存池初始化等地方,添加日志语句,记录相关信息,如分配的内存大小、释放的内存地址等 。通过查看日志文件,我们可以了解内存池的运行情况,追踪问题的发生过程 。比如,在每次分配内存时,记录分配的内存块地址和大小,这样在出现问题时,可以根据日志快速定位到问题发生的位置 。
优化内存池性能可以从多个方面入手 。减少内存碎片是一个重要的优化方向 。对于可变大小内存池,可以采用更智能的内存块合并算法,在释放内存块时,及时将相邻的空闲内存块合并成更大的内存块,减少内存碎片的产生 。同时,优化分配算法也能提高分配效率 。例如,对于固定大小内存池,可以使用更高效的链表操作方法,减少查找空闲内存块的时间 。在多线程环境下,还需要考虑线程安全问题,通过合理地使用锁机制或无锁数据结构,减少线程竞争,提高内存池在多线程环境下的性能 。
七、内存池应用场景
7.1高频内存分配释放场景
在高频内存分配和释放场景中,内存池的优势尤为明显 。以 Web 服务器为例,它需要处理大量的并发请求 。每个请求到来时,服务器都可能需要分配内存来存储请求数据、解析结果等 。当请求处理完成后,又需要释放这些内存 。如果使用传统的内存分配方式,频繁地调用malloc和free,不仅会增加系统开销,还容易产生内存碎片,降低服务器的性能 。
而采用内存池技术,服务器可以在启动时预先申请一块足够大的内存池 。当有请求到来时,直接从内存池中分配内存,请求处理结束后,将内存释放回内存池 。这样大大减少了内存分配和释放的时间开销,提高了服务器的响应速度 。据测试,在高并发情况下,使用内存池的 Web 服务器能够处理的请求数量比不使用内存池时提升 30% 以上 ,响应时间也能缩短 20% 左右 ,有效提升了服务器的性能和用户体验 。
7.2实时系统
实时系统对时间的要求极为严格,内存分配必须在极短的时间内完成,否则可能会导致系统响应延迟,影响整个系统的实时性 。例如,航空航天领域的飞行控制系统,它需要实时处理各种传感器数据,对飞机的飞行状态进行监控和调整 。在这个过程中,会频繁地进行内存分配和释放操作,以存储和处理传感器数据 。
如果使用传统的内存分配方式,由于其分配时间的不确定性,可能会导致飞行控制系统对某些关键数据的处理延迟,从而影响飞机的安全飞行 。而内存池可以预先分配好内存块,当系统需要内存时,能够在极短的时间内从内存池中获取,保证了系统的实时性 。在飞行控制系统中应用内存池后,数据处理的平均延迟从原来的几十毫秒降低到了几毫秒以内 ,大大提高了系统的实时响应能力,保障了飞行安全 。
7.3嵌入式系统
嵌入式系统通常资源有限,内存空间宝贵 。而且,嵌入式设备往往需要长时间稳定运行,不能因为内存问题而出现故障 。比如智能手环、智能家居设备等嵌入式设备,它们的内存容量相对较小 。在运行过程中,如果频繁地进行动态内存分配和释放,很容易导致内存碎片的产生,使得有限的内存空间变得更加碎片化,最终可能导致系统无法分配到足够的连续内存,出现内存不足的错误 。
内存池通过预先分配内存,并对内存块进行有效的管理,可以减少内存碎片的产生,提高内存利用率 。在一款智能手环的开发中,引入内存池后,内存利用率提高了 25% 左右 ,系统的稳定性也得到了显著提升,减少了因内存问题导致的设备死机和异常重启现象 ,延长了设备的使用寿命 。
7.4游戏开发
在游戏开发中,游戏对象的创建和销毁非常频繁 。例如,在一款动作游戏中,会不断地生成和销毁各种游戏角色、子弹、道具等对象 。每个游戏对象的创建都需要分配内存来存储其相关信息,销毁时则需要释放内存 。如果采用传统的内存分配方式,频繁的内存操作会消耗大量的时间,导致游戏运行不流畅,出现卡顿现象 ,严重影响玩家的游戏体验 。
使用内存池可以有效地解决这个问题 。游戏开发者可以根据游戏对象的类型和大小,创建相应的内存池 。当需要创建游戏对象时,直接从对应的内存池中获取内存,当游戏对象销毁时,将内存归还到内存池 。这样不仅提高了内存分配和释放的速度,还减少了内存碎片的产生 。在一款热门的手机动作游戏中,使用内存池后,游戏的帧率从原来的平均 40 帧提升到了 60 帧以上 ,游戏运行更加流畅,画面更加稳定,为玩家带来了更好的游戏体验 。
八、内存池避坑指南
在使用内存池的过程中,我们也需要注意一些潜在的问题,避免陷入 “坑” 中 。内存泄漏是一个常见的问题 。如果在内存释放过程中出现错误,比如忘记将释放的内存块标记为空闲,或者在多线程环境下,释放内存的操作被其他线程干扰,导致内存块没有正确归还到内存池,就会造成内存泄漏 。为了避免内存泄漏,在编写代码时,要仔细检查内存释放的逻辑,确保每个分配的内存块都能被正确释放 。可以使用智能指针等工具来辅助内存管理,减少手动管理内存带来的风险 。在多线程环境下,要确保内存释放操作的线程安全,合理地使用锁机制或无锁数据结构 。
内存溢出也是需要关注的问题 。如果内存池的大小设置不合理,或者程序在运行过程中内存需求突然增大,超过了内存池的容量,就可能导致内存溢出 。为了防止内存溢出,在初始化内存池时,要根据程序的实际需求,合理地设置内存池的大小 。可以通过对程序进行性能测试和分析,预估内存使用量,从而确定合适的内存池大小 。同时,在程序运行过程中,可以实时监控内存池的使用情况,当发现内存池即将耗尽时,及时采取措施,如动态扩展内存池的大小 。
性能瓶颈也是可能遇到的问题之一 。虽然内存池的设计初衷是提高内存分配和释放的效率,但如果设计或实现不当,反而可能导致性能瓶颈 。比如,分配算法选择不合理,导致查找合适内存块的时间过长;线程安全机制的实现过于复杂,增加了额外的开销等 。为了避免性能瓶颈,在设计内存池时,要选择合适的分配算法,根据程序的内存使用特点进行优化 。在多线程环境下,要合理地设计线程安全机制,减少锁竞争和其他额外开销 。可以通过性能测试工具,对内存池的性能进行分析和优化,找出潜在的性能瓶颈并加以解决 。