建议35:使用内存池技术提高内存申请效率与性能
Doug Lea曾有言曰:“自1960年以来,动态内存分配就已经成为大多计算机系统的重要部分。”
动态内存管理确实是件让人头疼的事儿,然而在实际的编程(www.cppentry.com)实践中,又不可避免地要大量用到堆上的内存。而这些通过malloc或new进行的内存分配却有着一些天生的缺陷:一方面,利用默认的内存管理函数在堆上分配和释放内存会有一些额外的开销,需要花费很多时间;另一方面,也是更糟糕的,随着时间的流逝,内存将形成碎片,一个应用程序的运行会越来越慢。
当程序中需要对相同大小的对象频繁申请内存时,常会采用内存池(Memory Pool)技术来提高内存申请效率。经典的内存池技术,是一种用于分配大量大小相同的小对象的技术。通过该技术可以极大地加快内存分配/释放过程。内存池技术通过批量申请内存,降低了内存申请次数,从而节省了时间。对于大批量的小对象而言,使用内存池技术整体申请内存,减少了内存碎片的产生,对性能提升的帮助也是很显著的。
内存池技术的基本原理通过这个“池”字就进行了很好的自我阐释:应用程序可以通过系统的内存分配调用预先一次性申请适当大小的内存块(Block),并会将它分成较小的块(Smaller Chunks),之后每次应用程序会从先前已经分配的块(chunks)中得到相应的内存空间,对象分配和释放的操作都可以通过这个“池”来完成。只有当“池”的剩余空间太小,不能满足应用程序需要时,应用程序才会再调用系统的内存分配函数对其大小进行动态扩展。
经典的内存池实现原理如下:
- class MemPool
- {
- public:
- MemPool(int nItemSize, int nMemBlockSize = 2048)
- : m_nItemSize(nItemSize),
- m_nMemBlockSize(nMemBlockSize),
- m_pMemBlockHeader(NULL),
- m_pFreeNodeHeader(NULL)
- {
- }
- ~ MemPool();
- void* Alloc();
- void Free();
- private:
- const int m_nMemBlockSize;
- const int m_nItemSize;
-
- struct _FreeNode
- {
- _FreeNode* pPrev;
- BYTE data[m_nItemSize - sizeof(_FreeNode*)];
- };
-
- struct _MemBlock
- {
- _MemBlock* pPrev;
- _FreeNode data[m_nMemBlockSize/m_nItemSize];
- };
-
- _MemBlock* m_pMemBlockHeader;
- _FreeNode* m_pFreeNodeHeader;
-
- };
其中MemPool涉及两个常量:m_nMemBlockSize、m_nItemSize,还有两个指针变量m_pMemBlockHeader、m_pFreeNodeHeader。指针变量m_pMemBlockHeader是用来把所有申请的内存块(MemBlock)串成一个链表。m_pFreeNodeHeader变量则是把所有自由的内存结点(FreeNode)串成一个链表。内存块在申请之初就被划分为了多个内存结点,每个结点的大小为ItemSize(对象的大小),共计MemBlockSize/ItemSize个。然后,这些内存结点会被串成链表。每次分配的时候从链表中取一个给用户,不够时继续向系统申请大块内存。在释放内存时,只须把要释放的结点添加到自由内存链表m_pFreeNodeHeader中即可。在MemPool对象析构时,可完成对内存的最终释放。
Boost库同样对该技术提供了较好的支持:
- pool(#include <boost/pool/pool.hpp>)
- boost::pool用于快速分配同样大小的小块内存。如果无法分配,返回0,如下所示。
- boost::pool<> p(sizeof(double)); //指定每次分配块的大小
- if(p!=NULL)
- {
- double* const d = (double*)p.malloc(); //为d分配内存
- pA.free(d); //将内存还给pool
- }
pool的析构函数会释放pool占用的内存。
- object_pool (#include <boost/pool/object_pool.hpp>)
object_pool和pool的区别在于:pool指定每次分配的块的大小,object_pool指定分配的对象的类型,如下所示: - boost::object_pool<A> p;
用A * pA = p.malloc()只会分配内存而不会调用构造函数,如果要调用构造函数应该使用A * const t = p.construct();- singleton_pool (#include <boost/pool/singleton_pool.hpp>)
singleton_pool和object_pool一样,不过它可以定义多个pool类型的object,给它们都分配同样大的内存块,另外singleton_pool提供静态方法分配内存,且不用定义对象,如下所示:- struct PoolTag{};
- typedef boost::singleton_pool<PoolTag,sizeof(int)> User_pool;
- int * const t = User_pool::malloc();
-
- my_pool::purge_memory(); //用完后释放内存
- pool_allocator (#include <boost/pool/pool_alloc.hpp>)
- pool_allocator基于singleton_pool实现,提供allocator,可用于STL等:
- std::vector<int, pool_allocator<int> > v;
- v.push_back(13);
- boost::singleton_pool<sizeof(int)>::release_memory(); //显式调用释放内存
由此可见,Boost确实是一个值得称赞、更值得使用的库,它能为我们提供极大的便利。当需要使用内存池技术时,请考虑Boost。
请记住:
当你需要频繁地分配相同大小的对象,而又苦恼于默认的内存管理函数带来的问题时,内存池技术将是灵丹妙药,它能提高内存操作效率,以及应用程序的鲁棒性。