C++内存池
内存池是一种内存分配方式。通常我们习惯直接使用new、malloc等API申请分配内存,这样做的缺点在于:由于所申请内存块的大小不定,当频繁使用时会造成大量的内存碎片。并由于频繁的分配和回收内存会降低性能,我们都知道,对象的构造和析构都是要花费时间的。
内存池也是一种对象池,我们在使用内存对象之前,先申请分配一定数量的内存块留作备用。当有新的内存需求时,就从内存池中分出一部分内存块,若内存块不够再继续申请新的内存。当不需要此内存时,重新将此内存放入预分配的内存块中,以待下次利用。这样合理的分配回收内存使得内存分配效率得到提升。
我们先看一个类。
1 class Rational 2 { 3 public: 4 Rational(int a = 0, int b = 1) :m(a), n(b) 5 { 6 } 7 private: 8 9 int m; 10 int n; 11 };
为了测试频繁的分配和回收此类对象我们写一个测试代码.
1 #include "stdafx.h" 2 #include <iostream> 3 #include <ctime> 4 5 using std::cout; 6 using std::cin; 7 using std::endl; 8 using std::minus; 9 10 #define R_SIZE 1000 11 12 int _tmain(int argc, _TCHAR* argv[]) 13 { 14 Rational *array[R_SIZE]; 15 double ft = .0f; 16 double st = .0f; 17 18 ft = clock(); 19 for (int i = 0; i < 500; ++i) 20 { 21 for (int j = 0; j < R_SIZE; ++j) 22 { 23 array[j] = new Rational; 24 } 25 for (int j = 0; j < R_SIZE; ++j) 26 { 27 delete array[j]; 28 } 29 30 } 31 st = clock(); 32 33 cout << minus<double>()(st, ft) << endl; 34 35 cin.get(); 36 37 return 0; 38 }
这样我们对Rational对象进行了100万次的对象分配和回收操作。这个代码在我的电脑上大概需要4890ms的执行时间
,
这是我们使用默认的内存管理器分配和回收。显然,如此频繁的分配以及回收会频繁的触发对象构造和析构函数,势必降低程序的运行。这就是好比我们要装水,我们没装一桶水都要去买水桶,用完了就卖出去,用到的时候再去买。这样不仅费时费力,而且还降低了我们的装水的效率。那我们要是买来适量是水桶,我们要用的时候拿出来用,不用的时候放回去,不够时再去买,这样省了我们不少的时间。这就是我们的内存池思想。我们现在写一个专用的Rational内存管理器。
我们声明一个NextOnFreeList类来连接空闲列表的相邻对象。
1 class NextOnFreeList 2 { 3 public: 4 NextOnFreeList *next; 5 };
class RationalMemPool { public: RationalMemPool(int a = 0, int b = 1) :m(a), n(b) { } /* * 重载new操作,如果对象链表中没有空闲将重新分配。 * 如果有空闲则从头部取出一个返回,并把空闲列表下移。 */ inline void *operator new(size_t _sz) { if (freeList == nullptr) { expandTheFreeList(); } NextOnFreeList *head = freeList; freeList = head->next; return head; } /* * 重载delete操作,将对象放回预分配链表中 */ inline void operator delete(void* _doomed, size_t _sz) { NextOnFreeList *head = static_cast<NextOnFreeList*>(_doomed); head->next = freeList; freeList = head; } static void newMemPool(){ expandTheFreeList(); } static void deleteMemPool(); private: static void expandTheFreeList(); enum{ EXPANSION = 50 }; //预分配对象数量 int m; int n; static NextOnFreeList *freeList; //空闲链表指针 }; NextOnFreeList * RationalMemPool::freeList = nullptr; /* * 使用完销毁对象 */ void RationalMemPool::deleteMemPool() { NextOnFreeList *nextPtr; for (nextPtr = freeList; nextPtr != nullptr; nextPtr = freeList) { freeList = freeList->next; delete[] nextPtr; } } /* * 事先分配一定数量的对象 */ void RationalMemPool::expandTheFreeList() { size_t size = (sizeof(RationalMemPool) > sizeof(NextOnFreeList*)) ? sizeof(RationalMemPool) : sizeof(NextOnFreeList*); NextOnFreeList *runner = reinterpret_cast<NextOnFreeList *>(new char[size]); freeList = runner; for (int i = 0; i < EXPANSION; ++i) { runner->next = reinterpret_cast<NextOnFreeList *>(new char[size]); runner = runner->next; } runner->next = 0; }
重新设计了Rational的内存分配策略后,我们再来看看测试代码的运行时间。
1 #include "stdafx.h" 2 #include <iostream> 3 #include <ctime> 4 5 using std::cout; 6 using std::cin; 7 using std::endl; 8 using std::minus; 9 10 #define R_SIZE 1000 11 12 int _tmain(int argc, _TCHAR* argv[]) 13 { 14 double ft = .0f; 15 double st = .0f; 16 RationalMemPool *array[R_SIZE]; 17 18 ft = clock(); 19 RationalMemPool::newMemPool(); 20 for (int i = 0; i < 500; ++i) 21 { 22 for (int j = 0; j < R_SIZE; ++j) 23 { 24 array[j] = new RationalMemPool(j); 25 } 26 for (int j = 0; j < R_SIZE; ++j) 27 { 28 delete array[j]; 29 } 30 } 31 RationalMemPool::deleteMemPool(); 32 st = clock(); 33 34 cout << minus<double>()(st, ft) << endl; 35 36 cin.get(); 37 38 return 0; 39 }
测试代码在我电脑上运行需要的时间为40ms
我们可以看到运行效率不只是一个级别。
参考《提高C++性能的编程技术》
/********************************2016-11-25更新****************************************/
我们在以上已经封装了一个专用的内存管理类了。那要是我们想让一个新类拥有类似的功能是不是也是要为这个类实现上面的成员呢,答案是的,不过我们可以通过一些技巧来完成,比如我们建立一个拥有这些能力的基类,再继承这个基类以拥有这些功能就可以了。
1 typedef unsigned char UCHAR; 2 3 template <class T, int BLOCK_NUM = 5> 4 class GenericMP 5 { 6 public: 7 static void *operator new(size_t allocLen) 8 { 9 assert(sizeof(T) == allocLen); 10 if (!m_NewPointer) 11 MyAlloc(); 12 UCHAR *rp = m_NewPointer; 13 m_NewPointer = *reinterpret_cast<UCHAR**>(rp); //由于头4个字节被“强行”解释为指向下一内存块的指针,这里m_NewPointer就指向了下一个内存块,以备下次分配使用。 14 return rp; 15 } 16 static void operator delete(void *dp) 17 { 18 *reinterpret_cast<UCHAR**>(dp) = m_NewPointer; 19 m_NewPointer = static_cast<UCHAR*>(dp); 20 } 21 22 static void deletePool() 23 { 24 UCHAR *p = m_NewPointer; 25 26 delete[] m_NewPointer; 27 m_NewPointer = nullptr; 28 } 29 private: 30 static void MyAlloc() 31 { 32 m_NewPointer = new UCHAR[sizeof(T)* BLOCK_NUM]; 33 34 memset(m_NewPointer, 0, sizeof(T)* BLOCK_NUM); 35 36 cout << sizeof(T)* BLOCK_NUM << endl; 37 UCHAR **cur = reinterpret_cast<UCHAR**>(m_NewPointer); //强制转型为双指针,这将改变每个内存块头4个字节的含义。 38 UCHAR *next = m_NewPointer; 39 for (size_t i = 0; i < BLOCK_NUM - 1; ++i) 40 { 41 next += sizeof(T); 42 *cur = next; 43 cur = reinterpret_cast<UCHAR**>(next); //这样,所分配的每个内存块的头4个字节就被“强行“解释为指向下一个内存块的指针, 即形成了内存块的链表结构。 44 } 45 *cur = 0; 46 } 47 static UCHAR *m_NewPointer; 48 protected: 49 ~GenericMP() 50 { 51 } 52 }; 53 template<class T, int BLOCK_NUM > 54 UCHAR *GenericMP<T, BLOCK_NUM >::m_NewPointer; 55 56 ////////////////////////////////////////////////////////////////////////// 57 class ExpMP : public GenericMP<ExpMP> 58 { 59 public: 60 UCHAR a[10]; 61 }; 62 ////////////////////////////////////////////////////////////////////////// 63 int _tmain(int argc, _TCHAR* argv[]) 64 { 65 ExpMP *aMP1 = new ExpMP(); 66 memcpy_s(aMP1->a + 4, 10, "aMP1", strlen("aMP1")); //由于头四个字节我们已经用来存放下一个对象的指针了,所以存在数据要从4个字节以后开始 67 68 ExpMP *aMP2 = new ExpMP(); 69 memcpy_s(aMP2->a + 4, 10, "aMP2", strlen("aMP2")); 70 71 delete aMP1; 72 delete aMP1; 73 74 ExpMP::deletePool(); 75 76 77 cin.get(); 78 79 return 0; 80 }