1 定长分块的内存池:
每次申请的内存量是常数。例如每次只能申请128字节,不多不少。
参考Scott Meyers effective C++定长分块的内存池。
这种内存池结构简单,易于实现和理解,效率也出奇的高(得益于自由列表数据结构)。
effective C++例子中需要为每个类都建立一个对应的内存池。
如果有100个类,就可能需要建立100个内存池。这种想法很自然,因为类的大小尺寸各不相同嘛。
反过来想,如果100个类的尺寸相同,就可以共享一个内存池。有可能吗?
让100个类尺寸相同是没有可能的,让100个类共享一个内存池是完全可能的。
我们计算100个类中尺寸最大的那一个,按此规格建立公共内存池,就能满足所有需求。
如果类A共80字节,类B有140字节。在申请A对象内存时,也至少配给140字节,这是浪费吗?
类A最大需要100个对象,类B最大需要1000个对象,预测这样的数据很难,我们按最大量配置
类A预先分配2000个对象,类B预先分配2000个对象,这样的浪费也比较大。如果使用公共内存池
,只需要为公共内存池分配2000个对象,这样就节省了2000个对象。
如何计算100个类尺寸最大值呢:不是人工算的,采用模板元编程技术,利用Loki库的Typelist
template <class T>
struct ExtractMaxSize
{
enum{ TailMaxSize = ExtractMaxSize<typename T::Tail>::MAXSIZE };
enum{ MAXSIZE = sizeof(T::Head) > TailMaxSize ? sizeof(T::Head) : TailMaxSize };
};
template <class T>
struct ExtractMaxSize< Typelist<T, NullType> >
{
enum { MAXSIZE = sizeof(T) };
};
2 变长分块的内存池:
支持从1字节到任意大小N字节的分配(不能超过内存池大小)。
支持变长量的申请,当然灵活,也带来固有不利性。比如内存碎片问题,申请效率也不如定长内存池高。
这里有一个简单构思。建立索引区和数据区,一共两个区。示意图如下:
_______________________________________________________________________________________
|_1_|_2_|_3_|_._|_._|_n_|____d1____|____d2____|____d3____|____.____|____.____|____dn____|
其中1-n为索引区的n个索引分块;d1-dn为数据区的n个数据分块。
实际需要把d1-dn分配出去,索引区的作用是:它是数据区的“模型”,
数据区的数据块的状态在索引区对应得分块上有体现和记录。
索引区的分块数等于数据区的分块数。在索引区查找满足需求的连续数据分块。
例如:找到99-200块是连续的自由块,在对应的索引块99-200做标记,将d99中适当地址分配给客户程序。
其实不需要在99-200将近100个索引块的每个块上写标记信息,只需要在99号和200号两个索引块上做标记。
在释放内存时,需要知道第d99那个地址对应的索引块。
我们可以将这种信息放到数据块首地址紧邻的前部,这些信息不能交给客户程序。
放大的d99数据块
_____________________________
|__索引ID__|___客户数据______
+
把+号指示的开始地址分配给客户程序。当未来某个时候需要释放这个内存时,我们能找到此块对应的索引。
混合型内存池:
我们把前两种内存池的优点集中起来。当请求的对象尺寸小于某个阈值时,交给固定尺寸内存池分配区处理。
当请求的对象尺寸大于某个阈值时,交给变长尺寸分块的内存池分配区处理。
根据实际情况,指定2种存储区规模,分块大小尺寸。
例如: 考虑80-20原则,80%请求是小块内存分配,20%请求是大块内存请求。
考虑黄金分割,把0.618比例的空间让给固定尺寸分配区
3 扩展
a) 支持内存池的按需增长。内存池不能“长”不太安全,可能发生No Memory错误。
我们可以开发一种支持多个“存储区”的内存池。数据块可能需要改一下:
放大的d99数据块
____________________________________________
|__索引ID__|__存储区ID____|___客户数据______
+
b) 支持多种存储介质。我指的是支持堆,支持全局静态区,支持共享内存,支持文件映射的内存等
把内存池布局和管理算法抽象出来,把存储空间的开辟算法剥离出来, 把线程同步剥离出来。
例子如下:
//模板类:内存池
template <class Layout =_Default_Layout , //布局策略:指定内存池规模,内存布局等
class SynMode=_Default_SynMode ,//同步模式:可采用互斥量,临界区等。
class Allocator =_Default_Alloctor> //分配策略:可以从堆中,等获得存储空间。
class CMemoryPool{ ...
c) 精简开销。由于我们的数据分块含有”夹带的私货“,管理区也需要一些开销,
所以尽量采用紧凑的内存布局。
例如:在内存池规模不大的前提下,用unsigned short表达内存分块ID号(能支持65535个块)
采用#pragma pack(1) 对齐。节省出来的管理空间做越界检查。
比如把这点空间填入aaa, 在检查时看这里存储的应该还是aaa
d) 优化STL分配器
让STL需要的内存来自内存池。在进程间通讯时,把共享内存的内存池交给STL分配器。
例如:考虑一个vector<int>。vector本身需要20字节,存放一些指针。
vector的分配器需要分配一些连续的空间存放int数组。
上述这些内存如果都来自共享内存池,则多个进程可以“看到”同样的vector数据。
windows不能保证,同一块共享内存,在所有进程对它“引进”地址空间时,都是相同的首地址值。
如果首地址不同,vector中存储的指针对有些进程就没有意义了。
例如:进程1 vector中有个指针m_pData=0x0012, 这个地址来自首地址是0x0010的共享内存“shasha”。
进程2 得到那个vector ,看到其指针m_pData=0x0012,但是进程2引入共享内存“shasha”时首地址是0x0020. 显然有些偏差了,把m_pData=0x0012改成m_pData=0x0022应该更正确呢。
但是调整STL内部数据谈何容易!还是通过某种机制,让进程2 引入共享内存“shasha”时首地址是0x0010吧.
幸好有个MapViewOfFileEx函数,最后一个参数就是干这个事的,但还是不能100%保证,您只能祈祷它成功了。
e) 同步
OS提供的malloc,free等效率不高的原因之一是,它们可能造成线程上下文切换,互斥量等待等。
自己定制的内存池可以不用互斥量,考虑用自旋锁。甚至在只有一个线程访问内存池的条件下,不用任何同步。
f) 多用静态检查机制,把错误扼杀在编译期。避免运行期的assert
template <bool assertion> struct StaticAssert;
template <> struct StaticAssert<true> { enum { CHECK = 1 }; };
enum { BLOCK_COUNT=编译期计算的分块总数 }
StaticAssert<(BLOCK_COUNT<65535)>::CHECK;