【内存池系列】内存池的一些设计思路

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;

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值