多进程编程多用在并发服务器的编写上,当收到一个请求时,服务器新建一个进程处理请求,同时继续监听。为了提高响应速度,服务器采用进程池的方法,在初始化阶段创建一个进程池,池中有许多预创建的进程,当请求到达时,只需从池中分配出来一个进程即可;当进程不够用时,进程池将再次创建一批进程。类似的方法可以用在内存分配上。
C++中,创建一个复杂的对象需要几十条指令,包括函数调用的代价(寄存器值得保存和恢复),以及构造或复制构造函数体的执行代价,甚至动态分配内存的代价。尤其是,在不重载new和delete运算符时,由于C++的new和delete是通用的,其中甚至提供了支持多线程编程的同步机制,对于单线程编程将带来不必要的运算。
如果需要创建的对象的个数是动态变化的,可以首先预开辟一片较大的内存,每次要创建对象时将一段内存分配出去。为了提高创建对象的速度,内存池应满足如下设计要求:
1,一定的通用性,适用于多类型,而不局限于某个类;
2,一定的灵活性,可以在不对类作过多修改的前提下,将内存池机制轻易融入到原代码中;
3,可能要满足多线程的要求。
4,通用性、灵活性视实际情况而定,不必完美。
一、专用内存池
先从专用内存池开始,假设要不断创建Complex (复数)类,下面的代码实现了针对Complex对象的内存池。代码最初来自《Effective C++》,笔者在看过书以后自行编写而成。
#include <iostream> #include <time.h> using namespace std; class SimpleComplex { double re; double im; public: explicit SimpleComplex(const double r = 0.0, const double i = 0.0):re(r), im(i) {} }; class Next { public: Next * next; }; class Complex { static Next * freelist; enum {EXPAND_SIZE = 2}; double re; double im; static void expand() { Next * p = (Next *) (new char [sizeof(Complex)]); freelist = p; for(int i=1; i<EXPAND_SIZE; i++) { p->next = (Next *) new char [sizeof(Complex)]; p = p->next; } p->next = 0; } public: explicit Complex(const double r = 0.0, const double i = 0.0):re(r), im(i) {} inline void * operator new (size_t size) { if (0 == freelist) expand(); Next * p = freelist; freelist = freelist->next; return p; } inline void operator delete(void * ptr, size_t size) { ((Next *)ptr)->next = freelist; freelist = (Next *)ptr; } static void newPool() { expand(); } static void deletePool() { Next * p = freelist; while (p) { freelist = freelist->next; delete [] p; p = freelist; } } }; Next * Complex::freelist = 0; main() { double from = time(0); Complex *c[100]; for (int i=0; i<1000000; i++) { for (int j=0; j<100; j++) c[j] = new Complex(); for (int j=0; j<100; j++) delete c[j]; } Complex::deletePool(); double to = time(0); cout <<to-from<<endl; from = time(0); SimpleComplex *s[100]; for (int i=0; i<1000000; i++) { for (int j=0; j<100; j++) s[j] = new SimpleComplex(); for (int j=0; j<100; j++) delete s[j]; } to = time(0); cout <<to-from<<endl; }
注意虽然有一个class next,但总体上看所有的内存块却并不组成链表。这里,内存是这么分配的:任何时刻freelist指向可用的内存块链表的头部,即第一个可用的内存块(单不足时,freelist指向NULL)。假设分别执行下面的语句:
p1 = new Complex (); p2 = new Complex (); p3 = new Complex (); p4 = new Complex (); delete p2; delete p4; p5 = new Complex ();
过程是这样的。Complex专属内存池在初始时刻不分配任何空间。给p1创建对象时,由于freelist指向NULL,先按照EXPAND_SIZE的值,开辟EXPAND_SIZE * sizeof(Complex)大小的内存。此时:
freelist -> [ next] -> [ next] -> NULL
随后将freelist指向的内存块分配给p1:
p1-> [ next] freelist-> [ next] -> NULL
随后将freelist指向的内存块分配给p2:
p1-> [ next] p2-> [ next] freelist -> NULL
随后为p3分配内存,在此之前再次执行expand() :
p1-> [ next] p2 -> [ next] p3 -> [ next] freelist -> [ next] -> NULL
随后将freelist指向的内存块分配给p4:
p1-> [ next] p2 -> [ next] p3 -> [ next] p4 -> [ next] freelist-> NULL
随后释放p2,内存池这时需要将p2的空间回收再利用,于是把p2的空间插入到freelist指向的链的前端:
p1-> [ next] p3 -> [ next] p4 -> [ next] freelist -> [(p2) next] -> NULL
随后释放p2,内存池这时需要将p2的空间回收再利用,于是把p2的空间插入到freelist指向的链的前端:
p1-> [ next] p3 -> [ next] freelist -> [(p4) next] -> [(p2) next] -> NULL
这样为p5开辟内存时,只需将原p4的内存给p5即可。
虽然池中的内存块不是以链表形式存在,当上述代码保证每个内存块都有一个指针变量指向它,最终在销毁时可以正常销毁。当然,如果仅仅写道
new Complex ();
这时创建的内存块就成了碎片,就算对于通用的new delete操作符而言,这样的碎片也是无可奈何的。注意,内存池并不实现像Java和C#这样自动回收内存的机制。
2, 通用固定大小内存池
上述内存池的实现嵌入在Complex代码中,只具备最低级的代码重用性(即复制粘贴方式的重用)。下面这个内存池通过模板的方式,适用于所有的单一类型。
#include <stdio.h> #include <time.h> class MemPoolNext { public: MemPoolNext * next; }; template <class T> class MemPool { enum {EXPAND_SIZE = 16}; static MemPoolNext * freelist; static void expand() { size_t size = sizeof(T); if(size < sizeof(void *)) size = sizeof(void *); MemPoolNext * p = (MemPoolNext *) new char [size]; freelist = p; for(int i=1; i<EXPAND_SIZE; i++, p=p->next) p->next = (MemPoolNext *) new char [size]; p->next = 0; } public: static void * alloc (size_t) { if(0 == freelist) expand(); void * p = freelist; freelist = freelist->next; return p; } static void free (void * p, size_t) { ((MemPoolNext *) p)->next = freelist; freelist = (MemPoolNext *) p; } static void deletepool() { MemPoolNext * p = freelist; while( p!=0 ) { freelist = p->next; delete p; p = freelist; } } }; template <class T> MemPoolNext * MemPool<T>::freelist = 0; class Complex { public: double re; double im; explicit Complex(double r = 0., double i = 0.): re(r), im(i){} inline static void * operator new (size_t s) { return MemPool<Complex>::alloc(s); } inline static void operator delete (void * p, size_t s) { MemPool<Complex>::free(p, s); } inline static void deletepool() { MemPool<Complex>::deletepool(); } }; main() { double from = time(0); Complex * c[100]; for (int i=0; i<1000000; i++) { for(int j=0; j<100; j++) c[j] = new Complex(j, j); for(int j=0; j<100; j++) delete c[j]; } Complex::deletepool(); double to = time(0); printf("%f\n", to-from); }
实际上很简单,无非是将原来写在Complex中用来管理内存的代码封装成了一个新的类MemPool。
三,多线程固定大小内存池
代码略,只需在临界区前后加上锁即可,最常用的锁是pthread_mutex_lock(信号量锁)。
四,多线程非固定大小内存池
此时,一个内存池内的对象已不局限于单一的类,而是能够同时包容不同类型的对象。代码略,只需在开辟和回收内存时,考虑当前可用的内存大小和已分配的内存大小即可。