- 浏览: 373350 次
- 性别:
- 来自: 苏州
文章分类
- 全部博客 (335)
- C++ (190)
- 设计模式 (43)
- 数据库技术 (5)
- 网络编程 (11)
- 自动化测试 (6)
- Linux (13)
- OpenSSL (10)
- MS Crypt API (5)
- SCM (2)
- English (4)
- Android (10)
- EMV规范 (1)
- Saturn Platform (0)
- C (10)
- SQL (2)
- ASP.NET (3)
- 英语口语学习 (3)
- 调试工具 (21)
- 编译技术 (5)
- UML (1)
- 项目管理 (5)
- 敏捷开发 (2)
- Http Server (6)
- 代码审查、代码分析 (5)
- 面试基础 (10)
- 重点知识 (16)
- STL (6)
- Efficient C++资料 (8)
- 数据结构和算法 (7)
- 读书笔记 (0)
- 开源项目 (4)
- 多线程 (2)
- Console App (6)
- 个人开源项目 (4)
- IBM DevelopWorks (4)
- Java (16)
- 内存泄漏相关调试和检测 (13)
- 软件测试相关技术 (2)
- C# (11)
- Apple Related (1)
- 软件测试和管理 (2)
- EMV (1)
- Python (1)
- Node.js (6)
- JavaScript (5)
- VUE (1)
- Frontend (1)
- Backend (4)
- RESTful API (3)
- Firebase (3)
最新评论
-
u013189503:
来个密码吧
[C++][Logging] 项目中写日志模块的实现 -
wyf_vc:
来个密码啊!!
[C++][Logging] 项目中写日志模块的实现
转自
http://www.cnblogs.com/rosesmall/archive/2012/04/27/2473931.html
http://www.codeproject.com/Articles/15527/C-Memory-Pool
http://www.codeproject.com/Articles/27487/Why-to-use-memory-pool-and-how-to-implement-it
优先推荐第三个实例:
具体思路如下:
1.首先申请一整块比较大的内存;
2.把一大块内存切割成等份的n块内存池;
3.管理未使用内存池列表和已使用内存池列表;
3.1 当使用时把内存块从未使用列表放到已使用列表;
3.2 当释放时把内存块从已使用列表放到未使用列表;
4.当内存不足或者没有合适的内存块时,重新申请一块内存;
5.对象使用结束,一次性释放所有的内存;
先看一个概念图:
全部代码参考附件MemoryPoolSourceCode.zip
Test Code
译者点评:一个简单的内存池实现,附有源码,简单易懂,适合入门。
概述
在c/c++中,内存分配(如malloc或new)会使用很多时间。
一个程序会随着长时间的运行和内存的申请释放而变得越来越慢,内存也会随着时间逐渐碎片化。特别是高频率的进行小内存申请释放,此问题变得尤其严重。
解决方案:定制内存池
为解决上述问题,一个(可能的)的解决方案就是使用内存池。
“内存池”在初始化时,分配一个大块内存(称 原始内存块),并且将此内存分割为一些小的内存块。当你需要请求分配内存时,则从内存池中取出事先分配好的内存,而不是向OS申请。内存池最大的优势在于:
1、极少的(甚至没有)堆碎片整理
2、较之普通内存分配(如malloc,new),有着更快的速度
额外的,你还将获得如下好处:
1、检测任意的指针是否指向内存池内
2、生成"heap-dump"
3、各种 内存泄漏 检测:当你没有释放之前申请的内存,内存池将抛出断言
如何工作?
让我们看看内存池的UML模型图:
图中简要的描述了CMemoryPool class,更多的细节请查看源码中class声明。
那么,CMemoryPool如何实际工作?
关于 MemoryChunks
正如你在UML图中所看到的,内存池维护着一个SMemoryChunk链表,并管理着三个指向SMemoryChunk结构的指针(m_ptrFirstChunk, m_ptrLastChunk, and m_ptrCursorChunk)。这些指针指向SMemoryChunk链表的不同位置。让我们更深入的观察SMemoryChunk:(在内存池实现中,SMemoryChunk封装了原始内存块的各个部分 -- 译者注)
typedef struct SMemoryChunk
{
TByte *Data ; // 常规数据指针
std::size_t DataSize ; // 内存块容量
std::size_t UsedSize ; // 内存块当前使用大小
bool IsAllocationChunk ; // 为true时, 内存块已被分配,可用free之类的函数释放
SMemoryChunk *Next ; // 指向内存块链表中的下一个内存块,可能为null
第一步:预分配内存
当你调用CMemoryPool的构造函数,内存池会向OS申请原始内存块。
/******************
Constructor
******************/
CMemoryPool::CMemoryPool(const std::size_t &sInitialMemoryPoolSize,
const std::size_t &sMemoryChunkSize,
const std::size_t &sMinimalMemorySizeToAllocate,
bool bSetMemoryData)
{
m_ptrFirstChunk = NULL ;
m_ptrLastChunk = NULL ;
m_ptrCursorChunk = NULL ;
m_sTotalMemoryPoolSize = 0 ;
m_sUsedMemoryPoolSize = 0 ;
m_sFreeMemoryPoolSize = 0 ;
m_sMemoryChunkSize = sMemoryChunkSize ;
m_uiMemoryChunkCount = 0 ;
m_uiObjectCount = 0 ;
m_bSetMemoryData = bSetMemoryData ;
m_sMinimalMemorySizeToAllocate = sMinimalMemorySizeToAllocate ;
// Allocate the Initial amount of Memory from the Operating-System...
AllocateMemory(sInitialMemoryPoolSize) ;
}
所有的成员的函数初始化在此完成,最后AllocateMemory将完成向OS申请原始内存块的任务。
/******************
AllocateMemory
******************/
<CODE>bool CMemoryPool::AllocateMemory(const std::size_t &sMemorySize)
{
std::size_t sBestMemBlockSize = CalculateBestMemoryBlockSize(sMemorySize) ;
// allocate from Operating System
TByte *ptrNewMemBlock = (TByte *) malloc(sBestMemBlockSize) ;
...
那么,内存池如何来管理这些数据呢?
第二步:内存分块
回忆前述,内存池管理使用SMemoryChunk链表来管理数据。在向OS申请原始内存块后,
我们还没有在其上建立SMemoryChunk。
图中所示的为初始化分配后的内存池。
我们需要分配一组SMemoryChunk,用于管理原始内存块:
//(AllocateMemory() continued) :
...
unsigned int uiNeededChunks = CalculateNeededChunks(sMemorySize) ;
// allocate Chunk-Array to Manage the Memory
SMemoryChunk *ptrNewChunks =
(SMemoryChunk *) malloc((uiNeededChunks * sizeof(SMemoryChunk))) ;
assert(((ptrNewMemBlock) && (ptrNewChunks))
&& "Error : System ran out of Memory") ;
...
CalculateNeededChunks函数用于计算需要分配的SMemoryChunk的数量。分配后,ptrNewChunks指向这组SMemoryChunk。注意,SMemoryChunk中目前只是持有垃圾数据,我们还没有为SMemoryChunk的成员关联至原始内存块。
最后,AllocateMemory函数将为所有的SMemoryChunk关联至原始内存块。
//(AllocateMemory() continued) :
...
// Associate the allocated Memory-Block with the Linked-List of MemoryChunks
return LinkChunksToData(ptrNewChunks, uiNeededChunks, ptrNewMemBlock) ;
让我们进入LinkChunksToData中一窥究竟:
/******************
LinkChunksToData
******************/
bool CMemoryPool::LinkChunksToData(SMemoryChunk *ptrNewChunks,
unsigned int uiChunkCount, TByte *ptrNewMemBlock)
{
SMemoryChunk *ptrNewChunk = NULL ;
unsigned int uiMemOffSet = 0 ;
bool bAllocationChunkAssigned = false ;
for(unsigned int i = 0; i < uiChunkCount; i++)
{
if(!m_ptrFirstChunk)
{
m_ptrFirstChunk = SetChunkDefaults(&(ptrNewChunks[0])) ;
m_ptrLastChunk = m_ptrFirstChunk ;
m_ptrCursorChunk = m_ptrFirstChunk ;
}
else
{
ptrNewChunk = SetChunkDefaults(&(ptrNewChunks[i])) ;
m_ptrLastChunk->Next = ptrNewChunk ;
m_ptrLastChunk = ptrNewChunk ;
}
uiMemOffSet = (i * ((unsigned int) m_sMemoryChunkSize)) ;
m_ptrLastChunk->Data = &(ptrNewMemBlock[uiMemOffSet]) ;
// 第一个SMemoryChunk被称为“AllocationChunk”。
// 这意味着,它持有原始内存块的指针并能够利用它释放原始内存块
if(!bAllocationChunkAssigned)
{
m_ptrLastChunk->IsAllocationChunk = true ;
bAllocationChunkAssigned = true ;
}
}
return RecalcChunkMemorySize(m_ptrFirstChunk, m_uiMemoryChunkCount) ;
}
让我们一步步的来看这个重要的函数:第一行检查在SMemoryChunk链表中是否已经有了可用的
SMemoryChunk:
...
if(!m_ptrFirstChunk)
...
在最初始进入循环,此条件不成立。那么,我们为一些内部的成员进行关联。
...
m_ptrFirstChunk = SetChunkDefaults(&(ptrNewChunks[0])) ;
m_ptrLastChunk = m_ptrFirstChunk ;
m_ptrCursorChunk = m_ptrFirstChunk ;
...
m_ptrFirstChunk这时指向SMemoryChunk中的第一个元素。每一个SMemoryChunk管理的内存块大小由m_sMemoryChunkSize指定。这些内存块来自于原始内存块,偏移量
uiMemOffSet指示着每一个SMemoryChunk所管理的内存起始点处于原始内存块的何处。
uiMemOffSet = (i * ((unsigned int) m_sMemoryChunkSize)) ;
m_ptrLastChunk->Data = &(ptrNewMemBlock[uiMemOffSet]) ;
额外的,每个新的SMemoryChunk都将被指定为新的m_ptrLastChunk。
...
m_ptrLastChunk->Next = ptrNewChunk ;
m_ptrLastChunk = ptrNewChunk ;
...
经过循环之后,内存池中的SMemoryChunk链表将被成功的与原始内存块关联。
最终,我们重新计算每一个SMemoryChunk能管理到的内存尺寸。这个步骤相当耗时,并且必须在每次从OS附加新的内存到内存池后调用。所有被计算出的尺寸,将被DataSize成员持有。
/******************
RecalcChunkMemorySize
******************/
bool CMemoryPool::RecalcChunkMemorySize(SMemoryChunk *ptrChunk,
unsigned int uiChunkCount)
{
unsigned int uiMemOffSet = 0 ;
for(unsigned int i = 0; i < uiChunkCount; i++)
{
if(ptrChunk)
{
uiMemOffSet = (i * ((unsigned int) m_sMemoryChunkSize)) ;
ptrChunk->DataSize =
(((unsigned int) m_sTotalMemoryPoolSize) - uiMemOffSet) ;
ptrChunk = ptrChunk->Next ;
}
else
{
assert(false && "Error : ptrChunk == NULL") ;
return false ;
}
}
return true ;
}
在RecalcChunkMemorySize之后,每一个SMemoryChunk将知道自己需要释放多大的内存。因此,这使得 确定某个SMemoryChunk能否持有一个指定大小的内存 将变得非常容易:当DataSize成员大于或等于请求的内存尺寸并且UsedSize成员值为0,这时此SMemoryChunk将能够满足用户的需要。让我们来看一个具体的例子来加深对这个机制的理解,假设内存池为600字节,并且每个SMemoryChunk为100字节。
第三步:向内存池请求内存
现在,如果用户向内存池请求内存,那会发生什么呢?最开始,所有的SMemoryChunk在内存池中都是闲置可用状态:
让我们看看GetMemory函数吧:
/******************
GetMemory
******************/
void *CMemoryPool::GetMemory(const std::size_t &sMemorySize)
{
std::size_t sBestMemBlockSize = CalculateBestMemoryBlockSize(sMemorySize) ;
SMemoryChunk *ptrChunk = NULL ;
while(!ptrChunk)
{
// 搜索是否有符合条件的SMemoryChunk?
ptrChunk = FindChunkSuitableToHoldMemory(sBestMemBlockSize) ;
if(!ptrChunk)
{
// 没有SMemoryChunk符合条件
// 内存池太小了,需要向OS申请新的内存
sBestMemBlockSize = MaxValue(sBestMemBlockSize, CalculateBestMemoryBlockSize(m_sMinimalMemorySizeToAllocate)) ;
AllocateMemory(sBestMemBlockSize) ;
}
}
// 一个合适的SMemoryChunk被找到
// 校正其 TotalSize/UsedSize 成员的值
m_sUsedMemoryPoolSize += sBestMemBlockSize ;
m_sFreeMemoryPoolSize -= sBestMemBlockSize ;
m_uiObjectCount++ ;
SetMemoryChunkValues(ptrChunk, sBestMemBlockSize) ;
// 最终将内存指针返回给用户
return ((void *) ptrChunk->Data) ;
}
当用户向内存池发出请求,内存池搜索SMemoryChunk链表,并在其中找到满足条件的SMemoryChunk,“满足条件”意味着:
1、DataSize必须大于或等于请求的大小
2、UsedSize必须为0
FindChunkSuitableToHoldMemory如果其返回NULL,那么就表示在内存池中没有可用的内存。这将会引发AllocateMemory函数的调用(前述),此函数会向OS申请更多的内存。
如果返回非NULL,那么便找到了可用的SMemoryChunk。
示例
假设,用户向内存池申请250字节:
如你所见,每一个SMemoryChunk管理100字节,所以,250字节并不是100的整数倍。这会引发什么情况呢?GetMemory将会返回指向第一个SMemoryChunk的指针,并设置其的UsedSize成员为300字节,因为300是100的整数倍数值中最小的,并且其大于250。多出的50字节称为"memory overhead".
当FindChunkSuitableToHoldMemory寻找可用的SMemoryChunk时,它将只会从一个闲置的SMemoryChunk跳到另一个闲置的SMemoryChunk。这意味着,如果又有申请内存的请求达到,例子中的第四个SMemoryChunk将是寻找的起始点。
如何使用代码
代码的使用简单而直接:
只需要在你的程序中包含"CMemoryPool.h",并附加源码文件至你的IDE/makefile:
CMemoryPool.h
CMemoryPool.cpp
IMemoryBlock.h
SMemoryChunk.h
你需要创建一个CMemoryPool实例,并从中分配内存。所有的内存池配置都在CMemoryPool的构造函数中被完成。
使用示例
MemPool::CMemoryPool *g_ptrMemPool = new MemPool::CMemoryPool() ;
char *ptrCharArray = (char *) g_ptrMemPool->GetMemory(100) ;
...
g_ptrMemPool->FreeMemory(ptrCharArray, 100) ;
delete g_ptrMemPool ;
兴趣点
内存诊断
你可以调用WriteMemoryDumpToFile函数来输出内存诊断信息文件。让我们看下源码附带的MyTestClass_OPOverload类的构造函数。(此类重载了new和delete操作,使用了内存池操作)
MyTestClass_OPOverload()
{
m_cMyArray[0] = 'H' ;
m_cMyArray[1] = 'e' ;
m_cMyArray[2] = 'l' ;
m_cMyArray[3] = 'l' ;
m_cMyArray[4] = 'o' ;
m_cMyArray[5] = NULL ;
m_strMyString = "This is a small Test-String" ;
m_iMyInt = 12345 ;
m_fFloatValue = 23456.7890f ;
m_fDoubleValue = 6789.012345 ;
Next = this ;
}
MyTestClass *ptrTestClass = new MyTestClass ;
g_ptrMemPool->WriteMemoryDumpToFile("MemoryDump.bin") ;
让我们看看内存诊断文件的内容:
如你所见,这是MyTestClass_OPOverload所有的成员在内存中的表示。
速度测试
我在windows下完成了一个简单的速度测试(使用timeGetTime()),结果显示内存池的使用可以大大增加程序的速度。所有的测试均使用vs2003,debug模式编译(测试机器:Intel Pentium IV Processor (32 bit), 1GB RAM, MS Windows XP Professional)
//Array-test (Memory Pool):
for(unsigned int j = 0; j < TestCount; j++)
{
// ArraySize = 1000
char *ptrArray = (char *) g_ptrMemPool->GetMemory(ArraySize) ;
g_ptrMemPool->FreeMemory(ptrArray, ArraySize) ;
}
//Array-test (Heap):
for(unsigned int j = 0; j < TestCount; j++)
{
// ArraySize = 1000
char *ptrArray = (char *) malloc(ArraySize) ;
free(ptrArray) ;
}
//Class-Test for MemoryPool and Heap (重载了new与delete)
for(unsigned int j = 0; j < TestCount; j++)
{
MyTestClass *ptrTestClass = new MyTestClass ;
delete ptrTestClass ;
}
关于代码
代码在ms windows与linux的如下c++编译器通过测试:
Microsoft Visual C++ 6.0
Microsoft Visual C++ .NET 2003
MinGW (GCC) 3.4.4 (Windows)
GCC 4.0.X (Debian GNU Linux)
vc6.0的项目文件与vs2003的项目文件已经包含在源码中。在64位的环境下使用应该没有问题。
注意:此内存池并非线程安全的。
待办事项
此内存池实现远远不够完善,待办事项如下:
1、对于海量的内存,memory overhead可能很大
2、一些CalculateNeededChunks函数的调用可以通过重构某些函数来被剥离,之后速度可能会更快。
3、更多的稳定性测试(尤其是对长时间运行的程序)
4、线程安全的实现
http://www.cnblogs.com/rosesmall/archive/2012/04/27/2473931.html
http://www.codeproject.com/Articles/15527/C-Memory-Pool
http://www.codeproject.com/Articles/27487/Why-to-use-memory-pool-and-how-to-implement-it
优先推荐第三个实例:
具体思路如下:
1.首先申请一整块比较大的内存;
2.把一大块内存切割成等份的n块内存池;
3.管理未使用内存池列表和已使用内存池列表;
3.1 当使用时把内存块从未使用列表放到已使用列表;
3.2 当释放时把内存块从已使用列表放到未使用列表;
4.当内存不足或者没有合适的内存块时,重新申请一块内存;
5.对象使用结束,一次性释放所有的内存;
先看一个概念图:
全部代码参考附件MemoryPoolSourceCode.zip
#ifndef __MEMPOOL_H__ #define __MEMPOOL_H__ class CMemPool { private: struct _Unit //The type of the node of linkedlist. { struct _Unit *pPrev, *pNext; }; void* m_pMemBlock; //The address of memory pool. //Manage all unit with two linkedlist. struct _Unit* m_pAllocatedMemBlock; //Head pointer to Allocated linkedlist. struct _Unit* m_pFreeMemBlock; //Head pointer to Free linkedlist. unsigned long m_ulUnitSize; //Memory unit size. There are much unit in memory pool. unsigned long m_ulBlockSize; //Memory pool size. Memory pool is make of memory unit. public: CMemPool(unsigned long lUnitNum = 50, unsigned long lUnitSize = 1024); ~CMemPool(); void* Alloc(unsigned long ulSize, bool bUseMemPool = true); //Allocate memory unit void Free( void* p ); //Free memory unit }; #endif //__MEMPOOL_H__
#include <stdio.h> #include <malloc.h> #include "MemPool.h" /*============================================================================== CMemPool: Constructor of this class. It allocate memory block from system and create a static double linked list to manage all memory unit. Parameters: [in]ulUnitNum The number of unit which is a part of memory block. [in]ulUnitSize The size of unit. //============================================================================= */ CMemPool::CMemPool(unsigned long ulUnitNum,unsigned long ulUnitSize) : m_pMemBlock(NULL), m_pAllocatedMemBlock(NULL), m_pFreeMemBlock(NULL), m_ulBlockSize(ulUnitNum * (ulUnitSize+sizeof(struct _Unit))), m_ulUnitSize(ulUnitSize) { m_pMemBlock = ::malloc(m_ulBlockSize); //Allocate a memory block. if(NULL != m_pMemBlock) { for(unsigned long i=0; i<ulUnitNum; i++) //Link all mem unit . { struct _Unit *pCurUnit = (struct _Unit *)( (char *)m_pMemBlock + i*(ulUnitSize+sizeof(struct _Unit)) ); pCurUnit->pPrev = NULL; pCurUnit->pNext = m_pFreeMemBlock; //Insert the new unit at head. if(NULL != m_pFreeMemBlock) { m_pFreeMemBlock->pPrev = pCurUnit; } m_pFreeMemBlock = pCurUnit; } } } /*============================================================================== ~CMemPool(): Destructor of this class. Its task is to free memory block. //============================================================================= */ CMemPool::~CMemPool() { ::free(m_pMemBlock); } /*============================================================================== Alloc: To allocate a memory unit. If memory pool can`t provide proper memory unit, will call system function. Parameters: [in]ulSize Memory unit size. [in]bUseMemPool Whether use memory pool. Return Values: Return a pointer to a memory unit. //============================================================================= */ void* CMemPool::Alloc(unsigned long ulSize, bool bUseMemPool) { if( ulSize > m_ulUnitSize || false == bUseMemPool || NULL == m_pMemBlock || NULL == m_pFreeMemBlock) { return malloc(ulSize); } //Now FreeList isn`t empty struct _Unit *pCurUnit = m_pFreeMemBlock; m_pFreeMemBlock = pCurUnit->pNext; //Get a unit from free linkedlist. if(NULL != m_pFreeMemBlock) { m_pFreeMemBlock->pPrev = NULL; } pCurUnit->pNext = m_pAllocatedMemBlock; if(NULL != m_pAllocatedMemBlock) { m_pAllocatedMemBlock->pPrev = pCurUnit; } m_pAllocatedMemBlock = pCurUnit; return (void *)((char *)pCurUnit + sizeof(struct _Unit) ); } /*============================================================================== Free: To free a memory unit. If the pointer of parameter point to a memory unit, then insert it to "Free linked list". Otherwise, call system function "free". Parameters: [in]p It point to a memory unit and prepare to free it. Return Values: none //============================================================================= */ void CMemPool::Free( void* p ) { if(m_pMemBlock<p && p<(void *)((char *)m_pMemBlock + m_ulBlockSize) ) { struct _Unit *pCurUnit = (struct _Unit *)((char *)p - sizeof(struct _Unit) ); m_pAllocatedMemBlock = pCurUnit->pNext; if(NULL != m_pAllocatedMemBlock) { m_pAllocatedMemBlock->pPrev = NULL; } pCurUnit->pNext = m_pFreeMemBlock; if(NULL != m_pFreeMemBlock) { m_pFreeMemBlock->pPrev = pCurUnit; } m_pFreeMemBlock = pCurUnit; } else { free(p); } }
Test Code
#include <Windows.h> #include <iostream> #include "..\MemoryPool\MemoryPool.h" using namespace std; // ////////////////////////////////////////////////////////////////////////// unsigned int g_nTotalRun = 100; unsigned int g_nCycleCount = 1000000/*0x5fffff*/; //CTestClass ////////////////////////////////////////////////////////////////////////// class CTestClassNoPool { public: char m_chBuf[1024]; }; void CCTestClassNoPool_RunTesting() { DWORD count = GetTickCount(); for(unsigned int i=0; i<g_nCycleCount; i++) { CTestClassNoPool *p = new CTestClassNoPool; delete p; } cout << "[Repeat " << g_nCycleCount << " Times]" << "[No pool] Interval = " << GetTickCount()-count << " ms" << endl; } // ////////////////////////////////////////////////////////////////////////// char buf[4100]; //Simple Memory Pool class CTestClassSimplePool { char m_chBuf[1024]; public: // CTestClassSimplePool() // { // cout << "CTestClassSimplePool" << endl; // } void *operator new(unsigned int uiSize) { return (void *)buf; } void operator delete(void *p) { } }; void CTestClassSimplePool_RunTesting() { DWORD count = GetTickCount(); for(unsigned int i=0; i<g_nCycleCount; i++) { CTestClassSimplePool *p = new CTestClassSimplePool; //Didn't use system new operator! delete p; } cout << "[Repeat " << g_nCycleCount << " Times]" << "[Using Simple pool] Interval = " << GetTickCount()-count << " ms" << endl; } ////////////////////////////////////////////////////////////////////////// class CTestClassMemoryPool { public: // CTestClassMemoryPool() // { // cout << "CTestClassMemoryPool" << endl; // } //* void *operator new(unsigned int uiSize); //Must overload the function. It`s static function. void operator delete(void *p); //Must overload the function. It`s static function. //*/ private: char m_chBuf[1024]; }; CMemoryPool g_MemPool(10,1024); //* void *CTestClassMemoryPool::operator new(unsigned int uiSize) { return g_MemPool.Alloc(uiSize); } void CTestClassMemoryPool::operator delete(void *p) { g_MemPool.Free(p); } void * operator new [](unsigned int uiSize) { return g_MemPool.Alloc(uiSize); } void operator delete[](void *p) { g_MemPool.Free(p); } void CTestClassMemoryPool_RunTesting() { DWORD count = GetTickCount(); for(unsigned int i=0; i< g_nCycleCount; i++) { //class CTestClass and class CTestClass2 are almost same! CTestClassMemoryPool *p = new CTestClassMemoryPool; //Using memory pool delete p; } cout << "[Repeat " << g_nCycleCount << " Times]" << "[Using memory pool] Interval = " << GetTickCount()-count << " ms" << endl; } ////////////////////////////////////////////////////////////////////////// void main() { cout << "How much new operations for each test case?" << endl; cin >> g_nCycleCount; cout << "How many times do you want to running?" << endl; cin >> g_nTotalRun; for (unsigned int i = 0 ; i< g_nTotalRun; i++) { cout << "----------------" << i+1 << "------------------" << endl; CCTestClassNoPool_RunTesting(); CTestClassSimplePool_RunTesting(); CTestClassMemoryPool_RunTesting(); } // CTestClassMemoryPool *p = new CTestClassMemoryPool[2]; //Using memory pool // delete[] p; }
译者点评:一个简单的内存池实现,附有源码,简单易懂,适合入门。
概述
在c/c++中,内存分配(如malloc或new)会使用很多时间。
一个程序会随着长时间的运行和内存的申请释放而变得越来越慢,内存也会随着时间逐渐碎片化。特别是高频率的进行小内存申请释放,此问题变得尤其严重。
解决方案:定制内存池
为解决上述问题,一个(可能的)的解决方案就是使用内存池。
“内存池”在初始化时,分配一个大块内存(称 原始内存块),并且将此内存分割为一些小的内存块。当你需要请求分配内存时,则从内存池中取出事先分配好的内存,而不是向OS申请。内存池最大的优势在于:
1、极少的(甚至没有)堆碎片整理
2、较之普通内存分配(如malloc,new),有着更快的速度
额外的,你还将获得如下好处:
1、检测任意的指针是否指向内存池内
2、生成"heap-dump"
3、各种 内存泄漏 检测:当你没有释放之前申请的内存,内存池将抛出断言
如何工作?
让我们看看内存池的UML模型图:
图中简要的描述了CMemoryPool class,更多的细节请查看源码中class声明。
那么,CMemoryPool如何实际工作?
关于 MemoryChunks
正如你在UML图中所看到的,内存池维护着一个SMemoryChunk链表,并管理着三个指向SMemoryChunk结构的指针(m_ptrFirstChunk, m_ptrLastChunk, and m_ptrCursorChunk)。这些指针指向SMemoryChunk链表的不同位置。让我们更深入的观察SMemoryChunk:(在内存池实现中,SMemoryChunk封装了原始内存块的各个部分 -- 译者注)
typedef struct SMemoryChunk
{
TByte *Data ; // 常规数据指针
std::size_t DataSize ; // 内存块容量
std::size_t UsedSize ; // 内存块当前使用大小
bool IsAllocationChunk ; // 为true时, 内存块已被分配,可用free之类的函数释放
SMemoryChunk *Next ; // 指向内存块链表中的下一个内存块,可能为null
第一步:预分配内存
当你调用CMemoryPool的构造函数,内存池会向OS申请原始内存块。
/******************
Constructor
******************/
CMemoryPool::CMemoryPool(const std::size_t &sInitialMemoryPoolSize,
const std::size_t &sMemoryChunkSize,
const std::size_t &sMinimalMemorySizeToAllocate,
bool bSetMemoryData)
{
m_ptrFirstChunk = NULL ;
m_ptrLastChunk = NULL ;
m_ptrCursorChunk = NULL ;
m_sTotalMemoryPoolSize = 0 ;
m_sUsedMemoryPoolSize = 0 ;
m_sFreeMemoryPoolSize = 0 ;
m_sMemoryChunkSize = sMemoryChunkSize ;
m_uiMemoryChunkCount = 0 ;
m_uiObjectCount = 0 ;
m_bSetMemoryData = bSetMemoryData ;
m_sMinimalMemorySizeToAllocate = sMinimalMemorySizeToAllocate ;
// Allocate the Initial amount of Memory from the Operating-System...
AllocateMemory(sInitialMemoryPoolSize) ;
}
所有的成员的函数初始化在此完成,最后AllocateMemory将完成向OS申请原始内存块的任务。
/******************
AllocateMemory
******************/
<CODE>bool CMemoryPool::AllocateMemory(const std::size_t &sMemorySize)
{
std::size_t sBestMemBlockSize = CalculateBestMemoryBlockSize(sMemorySize) ;
// allocate from Operating System
TByte *ptrNewMemBlock = (TByte *) malloc(sBestMemBlockSize) ;
...
那么,内存池如何来管理这些数据呢?
第二步:内存分块
回忆前述,内存池管理使用SMemoryChunk链表来管理数据。在向OS申请原始内存块后,
我们还没有在其上建立SMemoryChunk。
图中所示的为初始化分配后的内存池。
我们需要分配一组SMemoryChunk,用于管理原始内存块:
//(AllocateMemory() continued) :
...
unsigned int uiNeededChunks = CalculateNeededChunks(sMemorySize) ;
// allocate Chunk-Array to Manage the Memory
SMemoryChunk *ptrNewChunks =
(SMemoryChunk *) malloc((uiNeededChunks * sizeof(SMemoryChunk))) ;
assert(((ptrNewMemBlock) && (ptrNewChunks))
&& "Error : System ran out of Memory") ;
...
CalculateNeededChunks函数用于计算需要分配的SMemoryChunk的数量。分配后,ptrNewChunks指向这组SMemoryChunk。注意,SMemoryChunk中目前只是持有垃圾数据,我们还没有为SMemoryChunk的成员关联至原始内存块。
最后,AllocateMemory函数将为所有的SMemoryChunk关联至原始内存块。
//(AllocateMemory() continued) :
...
// Associate the allocated Memory-Block with the Linked-List of MemoryChunks
return LinkChunksToData(ptrNewChunks, uiNeededChunks, ptrNewMemBlock) ;
让我们进入LinkChunksToData中一窥究竟:
/******************
LinkChunksToData
******************/
bool CMemoryPool::LinkChunksToData(SMemoryChunk *ptrNewChunks,
unsigned int uiChunkCount, TByte *ptrNewMemBlock)
{
SMemoryChunk *ptrNewChunk = NULL ;
unsigned int uiMemOffSet = 0 ;
bool bAllocationChunkAssigned = false ;
for(unsigned int i = 0; i < uiChunkCount; i++)
{
if(!m_ptrFirstChunk)
{
m_ptrFirstChunk = SetChunkDefaults(&(ptrNewChunks[0])) ;
m_ptrLastChunk = m_ptrFirstChunk ;
m_ptrCursorChunk = m_ptrFirstChunk ;
}
else
{
ptrNewChunk = SetChunkDefaults(&(ptrNewChunks[i])) ;
m_ptrLastChunk->Next = ptrNewChunk ;
m_ptrLastChunk = ptrNewChunk ;
}
uiMemOffSet = (i * ((unsigned int) m_sMemoryChunkSize)) ;
m_ptrLastChunk->Data = &(ptrNewMemBlock[uiMemOffSet]) ;
// 第一个SMemoryChunk被称为“AllocationChunk”。
// 这意味着,它持有原始内存块的指针并能够利用它释放原始内存块
if(!bAllocationChunkAssigned)
{
m_ptrLastChunk->IsAllocationChunk = true ;
bAllocationChunkAssigned = true ;
}
}
return RecalcChunkMemorySize(m_ptrFirstChunk, m_uiMemoryChunkCount) ;
}
让我们一步步的来看这个重要的函数:第一行检查在SMemoryChunk链表中是否已经有了可用的
SMemoryChunk:
...
if(!m_ptrFirstChunk)
...
在最初始进入循环,此条件不成立。那么,我们为一些内部的成员进行关联。
...
m_ptrFirstChunk = SetChunkDefaults(&(ptrNewChunks[0])) ;
m_ptrLastChunk = m_ptrFirstChunk ;
m_ptrCursorChunk = m_ptrFirstChunk ;
...
m_ptrFirstChunk这时指向SMemoryChunk中的第一个元素。每一个SMemoryChunk管理的内存块大小由m_sMemoryChunkSize指定。这些内存块来自于原始内存块,偏移量
uiMemOffSet指示着每一个SMemoryChunk所管理的内存起始点处于原始内存块的何处。
uiMemOffSet = (i * ((unsigned int) m_sMemoryChunkSize)) ;
m_ptrLastChunk->Data = &(ptrNewMemBlock[uiMemOffSet]) ;
额外的,每个新的SMemoryChunk都将被指定为新的m_ptrLastChunk。
...
m_ptrLastChunk->Next = ptrNewChunk ;
m_ptrLastChunk = ptrNewChunk ;
...
经过循环之后,内存池中的SMemoryChunk链表将被成功的与原始内存块关联。
最终,我们重新计算每一个SMemoryChunk能管理到的内存尺寸。这个步骤相当耗时,并且必须在每次从OS附加新的内存到内存池后调用。所有被计算出的尺寸,将被DataSize成员持有。
/******************
RecalcChunkMemorySize
******************/
bool CMemoryPool::RecalcChunkMemorySize(SMemoryChunk *ptrChunk,
unsigned int uiChunkCount)
{
unsigned int uiMemOffSet = 0 ;
for(unsigned int i = 0; i < uiChunkCount; i++)
{
if(ptrChunk)
{
uiMemOffSet = (i * ((unsigned int) m_sMemoryChunkSize)) ;
ptrChunk->DataSize =
(((unsigned int) m_sTotalMemoryPoolSize) - uiMemOffSet) ;
ptrChunk = ptrChunk->Next ;
}
else
{
assert(false && "Error : ptrChunk == NULL") ;
return false ;
}
}
return true ;
}
在RecalcChunkMemorySize之后,每一个SMemoryChunk将知道自己需要释放多大的内存。因此,这使得 确定某个SMemoryChunk能否持有一个指定大小的内存 将变得非常容易:当DataSize成员大于或等于请求的内存尺寸并且UsedSize成员值为0,这时此SMemoryChunk将能够满足用户的需要。让我们来看一个具体的例子来加深对这个机制的理解,假设内存池为600字节,并且每个SMemoryChunk为100字节。
第三步:向内存池请求内存
现在,如果用户向内存池请求内存,那会发生什么呢?最开始,所有的SMemoryChunk在内存池中都是闲置可用状态:
让我们看看GetMemory函数吧:
/******************
GetMemory
******************/
void *CMemoryPool::GetMemory(const std::size_t &sMemorySize)
{
std::size_t sBestMemBlockSize = CalculateBestMemoryBlockSize(sMemorySize) ;
SMemoryChunk *ptrChunk = NULL ;
while(!ptrChunk)
{
// 搜索是否有符合条件的SMemoryChunk?
ptrChunk = FindChunkSuitableToHoldMemory(sBestMemBlockSize) ;
if(!ptrChunk)
{
// 没有SMemoryChunk符合条件
// 内存池太小了,需要向OS申请新的内存
sBestMemBlockSize = MaxValue(sBestMemBlockSize, CalculateBestMemoryBlockSize(m_sMinimalMemorySizeToAllocate)) ;
AllocateMemory(sBestMemBlockSize) ;
}
}
// 一个合适的SMemoryChunk被找到
// 校正其 TotalSize/UsedSize 成员的值
m_sUsedMemoryPoolSize += sBestMemBlockSize ;
m_sFreeMemoryPoolSize -= sBestMemBlockSize ;
m_uiObjectCount++ ;
SetMemoryChunkValues(ptrChunk, sBestMemBlockSize) ;
// 最终将内存指针返回给用户
return ((void *) ptrChunk->Data) ;
}
当用户向内存池发出请求,内存池搜索SMemoryChunk链表,并在其中找到满足条件的SMemoryChunk,“满足条件”意味着:
1、DataSize必须大于或等于请求的大小
2、UsedSize必须为0
FindChunkSuitableToHoldMemory如果其返回NULL,那么就表示在内存池中没有可用的内存。这将会引发AllocateMemory函数的调用(前述),此函数会向OS申请更多的内存。
如果返回非NULL,那么便找到了可用的SMemoryChunk。
示例
假设,用户向内存池申请250字节:
如你所见,每一个SMemoryChunk管理100字节,所以,250字节并不是100的整数倍。这会引发什么情况呢?GetMemory将会返回指向第一个SMemoryChunk的指针,并设置其的UsedSize成员为300字节,因为300是100的整数倍数值中最小的,并且其大于250。多出的50字节称为"memory overhead".
当FindChunkSuitableToHoldMemory寻找可用的SMemoryChunk时,它将只会从一个闲置的SMemoryChunk跳到另一个闲置的SMemoryChunk。这意味着,如果又有申请内存的请求达到,例子中的第四个SMemoryChunk将是寻找的起始点。
如何使用代码
代码的使用简单而直接:
只需要在你的程序中包含"CMemoryPool.h",并附加源码文件至你的IDE/makefile:
CMemoryPool.h
CMemoryPool.cpp
IMemoryBlock.h
SMemoryChunk.h
你需要创建一个CMemoryPool实例,并从中分配内存。所有的内存池配置都在CMemoryPool的构造函数中被完成。
使用示例
MemPool::CMemoryPool *g_ptrMemPool = new MemPool::CMemoryPool() ;
char *ptrCharArray = (char *) g_ptrMemPool->GetMemory(100) ;
...
g_ptrMemPool->FreeMemory(ptrCharArray, 100) ;
delete g_ptrMemPool ;
兴趣点
内存诊断
你可以调用WriteMemoryDumpToFile函数来输出内存诊断信息文件。让我们看下源码附带的MyTestClass_OPOverload类的构造函数。(此类重载了new和delete操作,使用了内存池操作)
MyTestClass_OPOverload()
{
m_cMyArray[0] = 'H' ;
m_cMyArray[1] = 'e' ;
m_cMyArray[2] = 'l' ;
m_cMyArray[3] = 'l' ;
m_cMyArray[4] = 'o' ;
m_cMyArray[5] = NULL ;
m_strMyString = "This is a small Test-String" ;
m_iMyInt = 12345 ;
m_fFloatValue = 23456.7890f ;
m_fDoubleValue = 6789.012345 ;
Next = this ;
}
MyTestClass *ptrTestClass = new MyTestClass ;
g_ptrMemPool->WriteMemoryDumpToFile("MemoryDump.bin") ;
让我们看看内存诊断文件的内容:
如你所见,这是MyTestClass_OPOverload所有的成员在内存中的表示。
速度测试
我在windows下完成了一个简单的速度测试(使用timeGetTime()),结果显示内存池的使用可以大大增加程序的速度。所有的测试均使用vs2003,debug模式编译(测试机器:Intel Pentium IV Processor (32 bit), 1GB RAM, MS Windows XP Professional)
//Array-test (Memory Pool):
for(unsigned int j = 0; j < TestCount; j++)
{
// ArraySize = 1000
char *ptrArray = (char *) g_ptrMemPool->GetMemory(ArraySize) ;
g_ptrMemPool->FreeMemory(ptrArray, ArraySize) ;
}
//Array-test (Heap):
for(unsigned int j = 0; j < TestCount; j++)
{
// ArraySize = 1000
char *ptrArray = (char *) malloc(ArraySize) ;
free(ptrArray) ;
}
//Class-Test for MemoryPool and Heap (重载了new与delete)
for(unsigned int j = 0; j < TestCount; j++)
{
MyTestClass *ptrTestClass = new MyTestClass ;
delete ptrTestClass ;
}
关于代码
代码在ms windows与linux的如下c++编译器通过测试:
Microsoft Visual C++ 6.0
Microsoft Visual C++ .NET 2003
MinGW (GCC) 3.4.4 (Windows)
GCC 4.0.X (Debian GNU Linux)
vc6.0的项目文件与vs2003的项目文件已经包含在源码中。在64位的环境下使用应该没有问题。
注意:此内存池并非线程安全的。
待办事项
此内存池实现远远不够完善,待办事项如下:
1、对于海量的内存,memory overhead可能很大
2、一些CalculateNeededChunks函数的调用可以通过重构某些函数来被剥离,之后速度可能会更快。
3、更多的稳定性测试(尤其是对长时间运行的程序)
4、线程安全的实现
- MemoryPool_demo.zip (105.3 KB)
- 下载次数: 0
- MemoryPool_src.zip (17.4 KB)
- 下载次数: 0
- C___Memory_Pool.zip (447.8 KB)
- 下载次数: 0
- MemoryPoolSourceCode.zip (21.8 KB)
- 下载次数: 0
- Why_to_use_memory_pool_and_how_to_implement_it.zip (127.3 KB)
- 下载次数: 0
发表评论
-
FreeRTOS
2022-03-05 16:31 253Ref https://blog.csdn.net/weix ... -
串口通讯相关
2018-11-02 13:44 417https://bbs.csdn.net/wap/topics ... -
[转]C++验证IP是否可以PING通
2018-10-30 17:54 1346https://www.cnblogs.com/guoyz13 ... -
C++/MFC 換皮膚
2018-10-20 11:05 481https://blog.csdn.net/u01123991 ... -
WinCE 截屏 - C++ 代碼
2018-08-31 09:45 580// this function create a bmp ... -
Android NDK搭建環境
2017-11-27 13:25 593https://www.cnblogs.com/ut2016- ... -
8583协议相关
2017-10-17 13:38 5828583相关资料,整理中... -
Java高级应用之JNI
2017-06-19 09:00 609参考link http://www.cnblogs.com/l ... -
C++实现ping功能
2017-04-18 11:21 2176基础知识 ping的过程是向目的IP发送一个type=8的I ... -
OpenSSL 编译环境搭建
2017-03-27 15:01 9161 安裝VS2008到 c:\Program Files (x ... -
最优非对称加密填充(OAEP)
2017-03-25 14:53 1596OpenSSL命令---rsautl http://blog. ... -
[Platform Builder] 设置SVM OS build Env
2016-11-10 11:39 01 copy one OSDesign Project to ... -
[Windows] System Error Codes(GetLastError )0-----5999
2016-10-26 13:28 1886ERROR_SUCCESS 0 (0x0) T ... -
开源Windows驱动程序框架
2016-09-17 21:35 878转自 http://code.csdn.net/news/28 ... -
c/c++代码中执行cmd命令
2016-09-14 14:50 1926转自 http://blog.csdn.net/slixinx ... -
C#使用C++标准DLL实例(包含callback)
2016-09-11 19:44 1095C++编写标准Win32DLL如下 头文件 /***** ... -
C#调用C++的DLL搜集整理的所有数据类型转换方式
2016-09-09 16:07 974转自 http://www.cnblogs.com/zeroo ... -
WinCE CPU使用率计算 测试工具
2016-09-08 16:14 1006转自 http://blog.csdn.net/jan ... -
switch在C++与C#中的一些差异
2016-09-08 15:19 821参考链接 http://blog.csdn.net/weiwe ... -
C++ 鼠标模拟程序
2016-09-04 12:09 1623转自 http://blog.csdn.net/weixinh ...
相关推荐
总的来说,C++内存池的实现是一个涉及内存管理、并发控制和性能优化的复杂任务。通过对内存池的持续改进和优化,可以显著提升程序的运行效率,特别是在需要大量频繁分配和释放小块内存的场景下。
本项目提供的是一个自定义实现的C++内存池,包括文档和源代码,旨在帮助理解内存池的工作原理,并提供实际应用的示例。 在传统的C++内存分配中,`new` 和 `delete` 操作会频繁地与操作系统交互,每次申请和释放内存...
在C/C++编程中,内存池常用于频繁创建和销毁小对象的场景,如网络编程、数据库连接等。本文将深入探讨几种内存池的实现方式及其源码分析。 1. **静态内存池**: 静态内存池在程序启动时就分配好内存,且在程序运行...
内存池的思想是,在真正使用内存之前,预先申请分配一定数量、大小预设的内存块留作备用。当有新的内存需求时,就从内存池中分出一部分内存块,若内存块不够再继续申请新的内存,当内存释放后就回归到内存块留作后续...
通过分析"MemPool"源码,我们可以学习如何设计和实现一个内存池,包括如何管理内存块,如何提高内存分配速度,以及如何在内存池中有效地查找、分配和回收内存。这对于编写高性能的C++程序,尤其是处理大量小对象的...
在C++11中,我们可以利用其新引入的特性来实现一个简单的内存池。本项目就是针对这一主题的一个实践,适用于Visual Studio 2015、g++4.8和clang++3.4等编译器。 内存池的基本思想是避免频繁的系统调用,因为传统的...
在C++中,内存池通过预分配一大块连续内存,并进行精细化管理来替代标准库中的new和delete操作。下面将详细阐述内存池的工作原理以及如何实现一个简单的内存池。 内存池的基本思想是预先分配一大块内存,然后根据...
内存池和自定义的缓冲区(BUFFER类)是提高程序效率和资源管理的重要技术。本文将深入探讨这两个主题,以及它们如何协同工作以优化多线程应用程序。 首先,让我们理解内存池的概念。内存池是一种内存管理策略,它...
总的来说,C++内存池是提升程序性能的重要工具,尤其在游戏开发、数据库系统等对内存管理要求较高的领域。通过对"memory_pool.zip"中的代码进行学习和分析,我们可以深入理解内存池的工作原理,并将其应用到实际项目...
总的来说,VS2010实现的内存池管理类`BufferPool`是针对C++内存管理的一种优化手段,通过预先分配和统一管理内存,以提升程序运行效率。通过深入理解并灵活运用内存池技术,开发者可以有效地控制和优化程序的内存...
总的来说,C和C++的内存池实现是一个涉及内存管理、性能优化和系统调用减少的关键技术,对于提高程序效率尤其是在资源受限的环境中至关重要。通过理解和实践内存池,开发者能够更好地控制和管理程序的内存使用,提升...
总的来说,C++内存池是提高程序性能的有效手段,尤其是在多线程和大量小对象分配的场景下。理解其工作原理并根据具体操作系统的特点进行优化,是每个C++开发者需要掌握的技能。通过阅读和分析提供的测试代码,我们...
《C++ 内存管理算法和实现》是一本深入探讨C++内存管理的权威著作,对于程序员来说,理解和掌握内存管理是提升编程技能的关键。内存管理不仅涉及到程序的效率,也直接影响到程序的稳定性和安全性。本文将围绕该书的...
《C++内存管理技术内幕【电子书】》深入探讨了C++编程语言中内存管理的核心技术和方法。在C++编程中,内存管理是一个基础而复杂的话题,尤其是内存泄露问题,长期困扰着许多程序员。本书的目的是帮助开发者理解和...
为了应对这一挑战,Nginx引入了一种名为“内存池”的内存管理机制。本文将深入探讨C++中Nginx内存池的源码与使用方法。 内存池是一种预先分配一大块连续内存,然后从中按需分配小块内存的策略。这种方式避免了系统...
std::allocator 是 C++标准库中提供的默认分配器,他的特点就在于我们在 使用 new 来申请内存构造新对象的时候,势必要调用类对象的默认构造函数 ...所以为了让代码直接可用,我们同样应该在内存池中设计同样的接口:
本主题的焦点是一个C++封装的内存池管理类,它设计用于管理类或自定义结构的内存,而不是基本数据类型。这是因为对于基本类型,如int、double等,C++的标准库已经提供了高效的内存管理机制,例如std::vector和std::...