内存池的引入——默认内存管理函数的不足
- new/delete或malloc/free分配和释放内存时会有一些额外开销
- 系统在接收到分配内存的请求时,首先要查找内部维护的内存空闲块表,并需要根据一定的算法,找到合适大小的空闲内存块。如果该空闲的内存块过大,则需要切割成已分配的部分和较小的内存块。然后系统更新内存空间块表,完成一次分配。在释放内存时,系统将释放的内存块重新加入到空闲内存块表中。也有可能把相邻的空闲块合成较大的空闲块。
- 默认的内存管理函数还考虑到多线程的应用,需要在每次分配和释放时加锁,同样增加了开销。
- 如果应用程序频繁的在堆内存上分配和释放内存,则会导致性能的损失,并且会使系统中出现大量的内存碎片,降低内存的利用率。
默认的分配和释放内存算法自然也考虑了性能,然而这些内存管理算法的通用版本为了应付更复杂、更广泛的情况,需要做更多的额外工作。而对于一个具体的应用程序来说,适合自身特定的内存分配释放模式的自定义内存池则可以获得更好的性能。
内存池的分类
应用程序自定义的内存池根据不同的使用场景有不同的类型
a.从线程安全角度内存池可分为单线程内存池和多线程内存池
- 单线程内存池整个生命周期只被一个线程使用,因而不需要考虑互斥访问的问题
- 多线程内存池有可能被多个线程共享,因此则需要在每次分配和释放内存时加锁
- 相对而言,单线程内存池性能更高,多线程进程池适用范围更广
b.从内存池可分配内存单元大小的角度可以将内存池分为固定内存池和可变内存池
- 固定内存池是指应用程序每次从内存池中分配出来的内存单元大小是固定不变的,维护起来方便,性能更高
- 可变内存池是指每次从内存分配出来的内存单元大小是可以按需变化的,应用范围更广,但是性能较低
内存池的思想及工作原理
应用程序可以通过系统的内存分配调用预先一次性申请适当大小的内存作为一个内存池,之后应用程序自己对内存的分配和释放则通过这个内存池来完成就可以了。只有当内存池的大小需要动态扩展时,才需要再调用系统的内存分配函数,其它时候对内存的一切操作都在应用程序的掌控之中。
固定内存池由一系列固定大小的内存块组成,每一个内存块又包含了固定数量和大小的内存单元。如上图,该内存池一共包含4个内存块,一个内存块包含一个内存块头和三个内存单元,在内存池初次生成时,只向系统申请了一个内存块,返回的指针作为整个内存池的头指针。之后随着应用程序对内存的不断需求,内存池判断需要动态扩大是,才再次向系统申请新的内存块,并把这些内存块再通过指针连接起来。由于大小是固定的的,所以分配的速度比较快;而对于应用程序来说,其内存池开辟了一定大小,内存池内部却还有剩余的空间。
如上图,我们将第四个内存块放大来看,其中包含一个内存池块头信息和3个大小相等的内存单元。假如内存单元1和内存单元2是空闲的,内存单元2已经被分配。当应用程序需要通过该内存池分配一个单元大小的内存时,只需要简单遍历所有的内存池块头信息,快速定位还有空闲内存单元的那个内存池快。然后根据该块的块头信息直接定位到第一个空闲的单元地址,把这个地址返回,并且标记下一个空闲单元即可;当应用程序释放某一个内存池单元时,直接在对应的内存池块头信息中标记该内存单元即可。
内存池的优点
- 针对特殊情况,如需要频繁的分配释放固定大小的内存对象时,不需要复杂的分配算法和多线程保护。也不需要维护内存空间标的额外开销,从而获得比较高的性能。
- 由于开辟了一定数量的连续内存空间作为内存池块,因而一定程度上提高了程序局部性,提升了程序性能。
- 比较容易控制页边界对齐和内存字节对齐,没有内存碎片的问题。
内存池的实现
内存池的内部构造
- 内存池的声明
class MemoryPool
{
private:
MemoryBlock* pBlock;//内存池中内存块链表的头指针
USHORT nUnitSize;//内存块中每个内存单元的字节大小
USHORT nInitSize;//第一个内存块内存单元的个数
USHORT nGrowSize//之后要增加的内存块的内存单元的个数
public:
MemoryPool(USHORT nUnitSize,USHORT nInitSize=1024,USHORT nGrowSize=256);
~Memory();
void * Alloc();//实际给程序分配内存单元的接口函数
void free(void *p);//程序释放并归还内存单元给内存池的接口函数
};
- MemoryBlock为内存池中附着在真正用来为内存请求分配内存的内存块头部的结构体,它描述了与之联系的内存块的使用信息。
-
struct MemoryBlock { USHORT nSize; //本内存块中内存单元的总字节大小 USHORT nFree; //本内存块中空闲的内存单元个数 USHORT nFirst; //本内存块中下一个可用的空闲内存单元的下标位置 USHORT nDummyAlign1; //用于字节对齐的2个字节,没有实际用处 MemoryBlock* pNext; //内存块链表的下一个指针 char aData[1]; //实际的内存单元开始的1字节,这里给出首地址 static void*operator new(size_t,USHORT nTypes,USHORT nUnitSize)// { return::operator new(sizeof(MemoryBlock)+nTypes*nUnitSize);// } static void operator delete(void *p,size_t) { ::operator delete(p);// } MemoryBlock(USHORT nTypes=1,USHORT nUnitSize=0);// ~MemoryBlock(){}//内存块不做析构释放内存,交给内存池统一管理 };