Boost.pool内存池

Boost库是一个可移植的开源C++函数库,鉴于STL(标准模板库)已经成为C++语言的一个组成部分,可以毫不夸张的说,Boost是目前影响最大的通用C++库。Boost库由C++标准委员会库工作组成员发起,其中有些内容有望成为下一代C++标准库内容,是一个标准库。

Boost内存池,即boost.pool库,是由Boost提供的一个用于内存池管理的开源C++库。作为Boost中影响较大的一个库,Pool已经被广泛使用。

1. 什么是内存池

是在计算机技术中经常使用的一种设计模式,其内涵在于:将程序中需要经常使用的核心资源先申请出来,放到一个池内,由程序自己管理,这样可以提高资源的使用效率,也可以保证本程序占有的资源数量。经常使用的池技术包括内存池、线程池和连接池等,其中尤以内存池和线程池使用最多。

  内存池(MemoryPool)是一种动态内存分配与管理技术。通常情况下,程序员习惯直接使用newdeletemallocfreeAPI申请分配和释放内存,导致的后果时:当程序长时间运行时,由于所申请内存块的大小不定,频繁使用时会造成大量的内存碎片从而降低程序和操作系统的性能。内存池则是在真正使用内存之前,先申请分配一大块内存(内存池)留作备用,当程序员申请内存时,从池中取出一块动态分配,当程序员释放内存时,将释放的内存再放入池内,并尽量与周边的空闲内存块合并。若内存池不够时,则自动扩大内存池,从操作系统中申请更大的内存池。

内存池的应用场景

  早期的内存池技术是为了专门解决那种频繁申请和释放相同大小内存块的程序,因此早期的一些内存池都是用相同大小的内存块链表组织起来的。

Boost的内存池则对内存块的大小是否相同没有限制,因此只要是频繁动态申请释放内存的长时间运行程序,都适用Boost内存池。这样可以有效减少内存碎片并提高程序运行效率。

安装

Boostpool库是以C++头文件的形式提供的,不需要安装,也没有lib或者dll文件,仅仅需要将头文件包含到你的C++工程中就可以了。Boost的最新版本可以到http://www.boost.org/下载。

2. 内存池的特征

2.1 无内存泄露

  正确的使用内存池的申请和释放函数不会造成内存泄露,更重要的是,即使不正确的使用了申请和释放函数,内存池中的内存也会在进程结束时被全部自动释放,不会造成系统的内存泄露。

2.2 申请的内存数组没有被填充

  例如一个元素的内存大小为A,那么元素数组若包含n个元素,则该数组的内存大小必然是A*n,不会有多余的内存来填充该数组。尽管每个元素也许包含一些填充的东西。

2.3 任何数组内存块的位置都和使用operator new[]分配的内存块位置一致

  这表明你仍可以使用那些通过数组指针计算内存块位置的算法。

2.4 内存池要比直接使用系统的动态内存分配快

  这个快是概率意义上的,不是每个时刻,每种内存池都比直接使用new或者malloc快。例如,当程序使用内存池时内存池恰好处于已经满了的状态,那么这次内存申请会导致内存池自我扩充,肯定比直接new一块内存要慢。但在大部分时候,内存池要比new或者malloc快很多。

测试1:连续申请和连续释放

3. 内存池效率测试

3.1测试1:连续申请和连续释放

  分别用内存池和new连续申请和连续释放大量的内存块,比较其运行速度,代码如下:

#include "stdafx.h"
#include 
#include 

#include 

#include 

#include 

using namespace 
std;
using namespace 
boost;

const int MAXLENGTH = 100000;

int main ( )
{
 boost::pool<> p(sizeof(int
));
 int* 
vec1[MAXLENGTH];
 int* 
vec2[MAXLENGTH];

 clock_t clock_begin = clock();
 for (int i = 0; i < MAXLENGTH; 
++i)
 
{
 vec1[i] = static_cast<int
*>(p.malloc());
 
}
 for (int i = 0; i < MAXLENGTH; 
++i)
 
{
 
p.free(vec1[i]);
 vec1[i] = 
NULL;
 
}

 clock_t clock_end = clock();
 cout<<"程序运行了 "<" 个系统时钟"<
 clock_begin = clock();
 for (int i = 0; i < MAXLENGTH; 
++i)
 
{
 vec2[i] = new int
;
 
}
 for (int i = 0; i < MAXLENGTH; 
++i)
 
{
 
delete vec2[i];
 vec2[i] = 
NULL;
 
}

 clock_end = clock();
 cout<<"程序运行了 "<" 个系统时钟"<
 return 0;
}

 

 测试环境:VS2008WindowXP SP2Pentium4 CPU双核,1.5GB内存。

  结论:在连续申请和连续释放10万块内存的情况下,使用内存池耗时是使用new耗时的47.46%

 

测试2:反复申请和释放小块内存

3.2测试2:反复申请和释放小块内存

  代码如下:

#include "stdafx.h"
#include 
#include 

#include 

#include 

#include 

using namespace 
std;
using namespace 
boost;

const int MAXLENGTH = 500000;

int main ( )
{
 boost::pool<> p(sizeof(int
));

 clock_t clock_begin = clock();
 for (int i = 0; i < MAXLENGTH; 
++i)
 
{
 int * t = static_cast<int
*>(p.malloc());
 
p.free(t);
 
}
 clock_t clock_end = 
clock();
 cout<<"程序运行了 "<" 个系统时钟"<
 clock_begin = clock();
 for (int i = 0; i < MAXLENGTH; 
++i)
 
{
 int* t = new int
;
 
delete t;
 
}
 clock_end = 
clock();
 cout<<"程序运行了 "<" 个系统时钟"<
 return 0;
}

  测试结果如下:

  结论:在反复申请和释放50万次内存的情况下,使用内存池耗时是使用new耗时的64.34%

 

测试3:反复申请和释放C++对象

3.3 测试3:反复申请和释放C++对象

C++对象在动态申请和释放时,不仅要进行内存操作,同时还要调用构造和析购函数。因此有必要对C++对象也进行内存池的测试。

  代码如下:

#include "stdafx.h"
#include 
#include 

#include 

#include 

#include 

using namespace 
std;
using namespace 
boost;

const int MAXLENGTH = 500000;
class 
A
{
public: 

 A()
 
{
 m_i++; 

 }
 
~A( )
 
{
 m_i--; 

 }
private
:
 int 
m_i;
};

int main ( )
{
 object_pool 
q;

 clock_t clock_begin = clock();
 for (int i = 0; i < MAXLENGTH; 
++i)
 
{
 A* a = 
q.construct();
 
q.destroy(a);
 
}

 clock_t clock_end = clock();
 cout<<"程序运行了 "<" 个系统时钟"<
 clock_begin = clock();
 for (int i = 0; i < MAXLENGTH; 
++i)
 
{
 A* a = new A; 

 delete a;
 
}
 clock_end = 
clock();
 cout<<"程序运行了 "<" 个系统时钟"<
 return 0;
}

  测试结果如下:

  结论:在反复申请和释放50万个C++对象的情况下,使用内存池耗时是使用new耗时的112.03%。这是因为内存池的constructdestroy函数增加了函数调用次数的原因。这种情况下使用内存池并不能获得性能上的优化。

 

Boost内存池的分类

 

4. Boost内存池的分类

Boost内存池按照不同的理念分为四类。主要是两种理念的不同造成了这样的分类。

  一是Object UsageSingleton Usage的不同。Object Usage意味着每个内存池都是一个可以创建和销毁的对象,一旦内存池被销毁则其所分配的所有内存都会被释放。Singleton Usage意味着每个内存池都是一个被静态分配的对象,直至程序结束才会被销毁,这也意味着这样的内存池是多线程安全的。只有使用release_memory或者purge_memory方法才能释放内存。

  二是内存溢出的处理方式。第一种方式是返回NULL代表内存池溢出了;第二种方式是抛出异常代表内存池溢出。

  根据以上的理念,boost的内存池分为四种。

4.1Pool

Pool是一个ObjectUsage的内存池,溢出时返回NULL

4.2object_pool

object_poolpool类似,唯一的区别是当其分配的内存释放时,它会尝试调用该对象的析购函数。

4.3singleton_pool

singleton_pool是一个SingletonUsage的内存池,溢出时返回NULL

4.4pool_alloc

pool_alloc是一个SingletonUsage的内存池,溢出时抛出异常。

5.内存池溢出的原理与解决方法

5.1必然溢出的内存

  内存池简化了很多内存方面的操作,也避免了一些错误使用内存对程序造成的损害。但是,使用内存池时最需要注意的一点是要处理内存池溢出的情况。

  没有不溢出的内存,看看下面的代码:

#include "stdafx.h"
#include 
#include 

#include 

#include 

#include 

using namespace 
std;
using namespace 
boost;

int _tmain(int argc, _TCHAR* argv[])
{
 clock_t clock_begin = 
clock();
 int iLength = 0
;
 for (int i = 0
; ;++i)
 
{
 void* p = malloc(1024*1024
);
 if (p == 
NULL)
 
{
 break
;
 
}
 
++iLength;
 
}
 clock_t clock_end = 
clock();
 cout<<"共申请了"<"M内存,程序运行了"<" 个系统时钟"<  return 0;
}

  运行的结果是共申请了1916M内存,程序运行了 69421 个系统时钟,意思是在分配了1916M内存后,malloc已经不能够申请到1M大小的内存块了。

 

内存池溢出的原理与解决方法

 

 内存池在底层也是调用了malloc函数,因此内存池也是必然会溢出的。而且内存池可能会比直接调用malloc更早的溢出,看看下面的代码:

#include "stdafx.h"
#include 
#include 

#include 

#include 

#include 

using namespace 
std;
using namespace 
boost;

int _tmain(int argc, _TCHAR* argv[])
{
 boost::pool<> pl(1024*1024
);
 clock_t clock_begin = 
clock();
 int iLength = 0
;
 for (int i = 0
; ;++i)
 
{
 void* p = 
pl.malloc();
 if (p == 
NULL)
 
{
 break
;
 
}
 
++iLength;
 
}
 clock_t clock_end = 
clock();
 cout<<"共申请了"<"M内存,程序运行了"<" 个系统时钟"<  return 0;
}

  运行的结果是共申请了992M内存,程序运行了 1265 个系统时钟,意思是在分配了992M内存后,内存池已经不能够申请到1M大小的内存块了。

5.2内存池的基本原理

  从上面的两个测试可以看出内存池要比malloc溢出早,我的机器内存是1.5Gmalloc分配了1916M才溢出(显然分配了虚拟内存),而内存池只分配了992M就溢出了。第二点是内存池溢出快,只用了1265微秒就溢出了,而malloc用了69421微秒才溢出。

  这些差别是内存池的处理机制造成的,内存池对于内存分配的算法如下,以pool内存池为例:

1.pool初始化时带有一个块大小的参数memSize,那么pool刚开始会申请一大块内存,例如其大小为32*memSize。当然它还会申请一些空间用以管理链表,为方便述说,这里忽略这些内存。

2.用户不停的申请大小为memSize的内存,终于超过了内存池的大小,于是内存池启动重分配机制;

3.重分配机制会再申请一块大小为原内存池大小两倍的内存(那么第一次会申请64*memSize),然后将这块内存加到内存池的管理链表末尾;

4.用户继续申请内存,终于又一次超过了内存池的大小,于是又一次启动重分配机制,直至重分配时无法申请到新的内存块。

5.由于每次都是两倍于原内存,因此当内存池大小达到了992M时,再一次申请就需要1984M,但是malloc最多只能申请到1916M,因此malloc失败,内存池溢出。

  通过以上原理也可以理解为什么内存池溢出比malloc溢出要快得多,因为它是以2的指数级来扩大内存池,真正调用malloc的次数约等于log2(1916),而malloc是实实在在进行了1916次调用。所以内存池只用了1秒多就溢出了,而malloc用了69秒。

5.3内存池溢出的解决方法

  对于malloc造成的内存溢出,一般来说没有太多办法可想。基本上就是报一个异常或者错误,然后让用户关闭程序。当然有的程序会有内存自我管理功能,可以让用户选择关闭一切次要功能来维持主要功能的继续运行。

  而对于内存池的溢出,还是可以想一些办法的,因为毕竟系统内存还有潜力可挖。

  第一个方法是尽量延缓内存池的溢出,做法是在程序启动时就尽量申请最大的内存池,如果在程序运行很久后再申请,可能OS因为内存碎片增多而不能提供最大的内存池。其方法是在程序启动时就不停的申请内存直到内存池溢出,然后清空内存池并开始正常工作。由于内存池并不会自动减小,所以这样可以一直维持内存池保持最大状态。

  第二个方法是在内存池溢出时使用第二个内存池,由于第二个内存池可以继续申请较小块的内存,所以程序可继续运行。代码如下:

#include "stdafx.h"
#include 
#include 

#include 

#include 

#include 

using namespace 
std;
using namespace 
boost;

int _tmain(int argc, _TCHAR* argv[])
{
 boost::pool<> pl(1024*1024
);
 clock_t clock_begin = 
clock();
 int iLength = 0
;
 for (int i = 0
; ;++i)
 
{
 void* p = 
pl.malloc();
 if (p == 
NULL)
 
{
 break
;
 
}
 
++iLength;
 
}
 clock_t clock_end = 
clock();
 cout<<"共申请了"<"M内存,程序运行了"<" 个系统时钟"<
 clock_begin = clock();
 iLength = 0
;
 boost::pool<> pl2(1024*1024
);
 for (int i = 0
; ;++i)
 
{
 void* p = 
pl2.malloc();
 if (p == 
NULL)
 
{
 break
;
 
}
 
++iLength;
 
}
 clock_end = 
clock();
 cout<<"又申请了"<"M内存,程序运行了"<" 个系统时钟"<
 return 0;
}

  运行结果如下:

  结果表明在第一个内存池溢出后,第二个内存池又提供了480M的内存。

5.4内存池溢出的终极方案

  如果无论如何都不能再申请到新的内存了,那么还是老老实实告诉用户重启程序吧。


6、 singleton_pool 接口

它与 pool 接口几乎相同,但是用作独立池。独立池的底层结构具有为 mallocfree 等声明的静态成员函数,并且构造函数是私有的。独立池声明中的第一个参数称为标记——它允许存在不同的独立池集(例如,用于 int 的多个池,其中每个池服务于不同的目的)。必须包括 singleton_pool.hpp 头文件才能使用此接口


例子

#include
#include
using namespace std;
using namespace boost;


struct intpool {  };
struct intpool2 {  };


typedef boost::singleton_pool ipool1;
typedef boost::singleton_pool ipool2;


int main ( )
{
  cout << "Init singleton pool...\n";
  for (int i=0; i<10; ++i)
  {
    int* q1 = (int*) ipool1::malloc();
    int* q2 = (int*) ipool2::malloc();
  }
  ipool1::purge_memory();
  ipool2::purge_memory();
  return 0;
}

7 、 pool_alloc 接口


它通常与 STL 容器结合在一起使用。请考虑以下代码片段:

#include


std::vector > v;

std::list > L;

Pool分配是一种分配内存方法,用于快速分配同样大小的内存块,尤其是反复分配/释放同样大小的内存块的情况。 
1. pool
快速分配小块内存,如果pool无法提供小块内存给用户,返回0。
Example:
void func()
{
boost::pool<> p(sizeof(int));
^^^^^^^^^^^
指定每次分配的块的大小
for (int i = 0; i < 10000; ++i)
{
int * const t = p.malloc();
pool分配指定大小的内存块;需要的时候,pool会向系统
申请大块内存。
... // Do something with t; don't take the time to free() it
p.free( t );
// 释放内存块,交还给pool,不是返回给系统。
}
pool的析构函数会释放所有从系统申请到的内存。
2. object_pool 
与pool的区别在于:pool需要指定每次分配的块的大小,object_pool需要指定每次分配的对象的类型。
Example:
struct X { ... }; // has destructor with side-effects
void func()
{
boost::object_pool p;
^
for (int i = 0; i < 10000; ++i)
{
X * const t = p.malloc();
注意;X的构造函数不会被调用,仅仅是分配大小为sizeof(X)
的内存块。如果需要调用构造函数(像new一样),应该调用
construct。比如:
X * const t = p.construct();
...
}
}


3. singleton_pool
与pool用法一样。不同的是:可以定义多个pool类型的object,都是分配同样
大小的内存块;singleton_pool提供静态成员方法分配内存,不用定义object。
Example:
struct MyPoolTag { };
typedef boost::singleton_pool my_pool;
void func()
{
for (int i = 0; i < 10000; ++i)
{
int * const t = my_pool::malloc();
// ^^^^^^^^^
// 和pool不一样。
...
}
my_pool::purge_memory();
// 释放my_pool申请的内存。
}
4. pool_alloc
基于singleton_pool实现,提供allocator(用于STL等)。
Example:
void func()
{
std::vector > v;
for (int i = 0; i < 10000; ++i)
v.push_back(13);
}
需要的话,必须自己显式地调用
boost::singleton_pool::release_memory()
把allocator分配的内存返回系统。
实现原理
pool每次向系统申请一大块内存,然后分成同样大小的多个小块,形成链表连接起来。每次分配的时候,从链表中取出头上一块,提供给用户。链表为空的时候,pool继续向系统申请大块内存。
一个小问题:在pool的实现中,在申请到大块内存后,马上把它分成小块形成链表。这个过程开销比较大。即你需要分配一小块内存时,却需要生成一个大的链表。用如下代码测试:
boost::pool<> mem_pool(16);
for(i = 0; i < NPASS; i++) {
period = clock();
for(n = 0; n < NITEM; n++) {
array_ptr[n] = (int *)mem_pool.malloc();
}
for(n = 0; n < NITEM; n++) {
mem_pool.free(array_ptr[n]);
}
period = clock() - period;
printf("pool<> : period = ] ms ", period);
}
可以发现,第一遍花的时间明显多于后面的。
而且在pool的使用过程中如果不是恰好把链表中所有的小块都用上的话,在链表中最后的一些小块会始终用不上。把这些小块加入链表是多余的。虽然这个开销可能很小:)

你可能感兴趣的:(Boost.pool内存池)