纯代码解析 C++内存池的简单原理及实现

一、为什么需要使用内存池在C/C++中我们通常使用malloc,free或new,delete来动态分配内存 。
一方面 , 因为这些函数涉及到了系统调用 , 所以频繁的调用必然会导致程序性能的损耗;
另一方面 , 频繁的分配和释放小块内存会导致大量的内存碎片的产生 , 当碎片积累到一定的量之后 , 将无法分配到连续的内存空间 , 系统不得不进行碎片整理来满足分配到连续的空间 , 这样不仅会导致系统性能损耗 , 而且会导致程序对内存的利用率低下 。
当然 , 如果我们的程序不需要频繁的分配和释放小块内存 , 那就没有使用内存池的必要 , 直接使用malloc,free或new,delete函数即可 。
二、内存池的实现方案内存池的实现原理大致如下:
提前申请一块大内存由内存池自己管理 , 并分成小片供给程序使用 。程序使用完之后将内存归还到内存池中(并没有真正的从系统释放) , 当程序再次从内存池中请求内存时 , 内存池将池子中的可用内存片返回给程序使用 。
我们在设计内存池的实现方案时 , 需要考虑到以下问题:
内存池是否可以自动增长?
如果内存池的最大空间是固定的(也就是非自动增长) , 那么当内存池中的内存被请求完之后 , 程序就无法再次从内存池请求到内存 。所以需要根据程序对内存的实际使用情况来确定是否需要自动增长 。
内存池的总内存占用是否只增不减?
如果内存池是自动增长的 , 就涉及到了“内存池的总内存占用是否是只增不减”这个问题了 。试想 , 程序从一个自动增长的内存池中请求了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;
 
三、内存池的具体实现计划实现一个内存池管理的类MemoryPool , 它具有如下特性:

  1. 内存池的总大小自动增长 。
  2. 内存池中内存片的大小固定 。
  3. 支持线程安全 。
  4. 在内存片被归还之后 , 清除其中的内容 。
  5. 兼容std::allocator 。
因为内存池的内存片的大小是固定的 , 不涉及到需要匹配最合适大小的内存片 , 由于会频繁的进行插入、移除的操作 , 但查找比较少 , 故选用链表数据结构来管理内存池中的内存片 。
MemoryPool中有2个链表 , 它们都是双向链表(设计成双向链表主要是为了在移除指定元素时 , 能够快速定位该元素的前后元素 , 从而在该元素被移除后 , 将其前后元素连接起来 , 保证链表的完整性):


推荐阅读