缓冲区池 \ 对象池






4.50/5 (17投票s)
2003年1月19日
5分钟阅读

136630

3193
一篇关于动态内存使用优化的文章。
引言
任何处理在运行时频繁分配和释放大量动态内存的应用程序的人都知道,这会导致性能下降,因为这些操作需要某种全局锁来保护,因此不同的模块会相互影响性能。此外,随着时间的推移,内存会因各种大小的分配而碎片化,如果一个应用程序在机器上长期驻留,其性能会随着时间的推移而下降。
在本文中,我将尝试提供一个部分解决方案。为什么是部分?因为我不是试图在操作系统之上提供更好的内存管理器实现。我能优化的只是那些在运行过程中频繁分配和释放相同大小缓冲区的模块,根据我的经验,这是最常见的。
我建议的解决方案是创建一个*缓冲区池*,它将分配和释放预定义大小的缓冲区,分配和释放操作的复杂度为 O(1),并且每个*缓冲区池*对象都是自同步的,因此不会影响使用其他*缓冲区池*的其他模块。此外,*缓冲区池*的动态分配是在大小为 N 个缓冲区的内存段中进行的,因此内存碎片化会大大减少。更重要的是,因为每个段都作为一个内存块分配,内存分页也会减少。
使用代码
首先声明一个 CBufferPool
对象
CBufferPool BuffPoolObj;
接下来通过调用 Create
方法来创建*缓冲区池*。
BuffPoolObj.Create(nBufferSize, //Buffer Size nBuffersPerSegment, //Number of buffers in each segment nInitialNumberOfSegments, //Initial number of segments nMinNumberOfSegments, //Minimum number of segments during run nMaxNumberOfSegments, //Maximum number of segments during run nFreeBuffersPercentForSegmentDeletion); //Percentage of all free buffers relative to //size of a segment that allows a segments deletion
最后一个参数 nFreeBuffersPercentForSegmentDeletion
的理解可能不太直观,我将尝试解释其目的。在*缓冲区池*的实现中,缓冲区是从预先分配的段中获取的。存在一个“灰色区域”,如果一个缓冲区是段中的第一个分配缓冲区,我们会为其分配一个新的段,而如果在下一个操作中释放同一个缓冲区,我们应该释放整个段等等。这会导致性能下降,比我们以传统方式分配缓冲区还要糟糕。为了防止这种情况发生,如果我有一定数量的空闲缓冲区(来自整个段),我将释放一个段。这个安全距离由该参数确定。
然后你可以调用 Allocate
方法来获取所需的缓冲区,并调用 Free
方法将其返回到池中。
void* pBuffer = BuffPoolObj.Allocate() //do something with buffer BuffPoolObj.Free(pBuffer);
CBufferPool
具有同步机制,因此可以异步调用这些方法。要销毁*缓冲区池*对象,请调用 Destroy
方法。此方法将释放所有段,在调试模式下,它将检查所有缓冲区是否都已释放。
使用演示
演示项目演示了*缓冲区池*如何比简单的 new
和 delete
运算符提高程序的性能。此外,它还可以用来检查你的系统是否可以改进,以及使用特定配置可以实现的改进的大致程度。
演示项目有 3 个测试可以执行:测试*缓冲区池*,然后测试全局分配(new
和 delete
)以查看*缓冲区池*相对于全局分配的改进,以及交互测试,它可以说明与*缓冲区池*相比,全局分配在性能下降方面遭受的严重影响。
演示中的参数分为三部分:用于运行*缓冲区池*和全局分配的会话参数,以及另外两个分别针对两者的部分。对于不直观的参数,你可以通过代码来理解。*缓冲区池*代码有非常好的文档记录,我尽量让演示应用程序代码保持简单。
对于那些在调试模式下编译演示的人,我只想说,在调试模式下添加了*缓冲区池*测试,以确保被释放的缓冲区或段不会被使用,并且正在分配的缓冲区不会已经被使用。这些测试(通常是 memset 来破坏内存使其不可用)会显著降低性能,所以请在发布模式下进行测试。
实现
我将尝试从整体上解释实现。*缓冲区池*由一个双向链表组成,每个段都有一个单向链表来存储空闲缓冲区。缓冲区之间的链接使用缓冲区本身。每个缓冲区除了所需的内存外,还有一个头部,用于保存其在段中的索引,因此释放操作将直接进行,无需搜索,其复杂度为 O(1)。段和缓冲区的所有内存都作为一个块进行分配。段列表在运行时进行排序,如果段已满,它将被插入到列表的末尾,如果它有一个或多个缓冲区被释放,它将移动到列表的头部。分配操作只是从列表中弹出一个空闲缓冲区。当释放一个缓冲区时,会进行计算以提取其所在的段。每次没有空闲缓冲区时,就会分配一个段,并且当*缓冲区池*中的空闲缓冲区总数超过 nFreeBuffersPercentForSegmentDeletion
乘以 nBuffersPerSegment
时,段将被释放。这是基本思想,具体细节请参见代码,代码记录得非常好。
对象池
*对象池*是一个对象池的实现,它是*缓冲区池*的直接后代。要使用它,你需要声明一个具有已声明类型的*对象池*。
CObjectPool<SomeType> SomeTypeObjectPool;
接下来,通过调用 Create
方法创建*对象池*。
SomeTypeObjectPool.Create ( nObjectsPerSegment, //Number of objects in each segment nInitialNumberOfSegments, //Initial number of segments nMinNumberOfSegments, //Minimum number of segments during run nMaxNumberOfSegments, //Maximum number of segments during run nFreeObjectsPercentForSegmentDeletion); //Percentage of all free objects //in relative to the size of a segment that allows a segments deletion
池的使用如下:
SomeType* pObject = SomeTypeObjectPool.Allocate()
//do something with object
SomeTypeObjectPool.Free(pObject);
*对象池*实现的代码量非常少,它使用了一个 new
和 delete
的“placement”(如文档中所称呼的方法),该方法接收指向*缓冲区池*的指针,该*缓冲区池*在 Create
方法中以对象大小作为缓冲区大小进行初始化。我认为这个实现非常令人兴奋,因为它使用了很少使用的 new
\delete
“placement” 方法。
在 CDemoDlg
的 OnInitDialog
函数中展示了一个*对象池*的简单示例。
你可以在任何平台上使用 CBufferPool
\ CObjectPool
。你只需要将临界区对象替换为其他平台相关的互斥锁即可。
感谢 George Anescu 提供演示项目中使用的 CPreciseTimer
类。