这是我翻译的文章,来自 Code Project,
原文作者: DanDanger2000.
原文链接: http://www.codeproject.com/cpp/MemoryPool.asp
C++ 内存池
l 下载示例工程 – 105Kb
l 下载源代码 – 17.3Kb
目录
l
引言
l
它怎样工作
l
示例
l
使用这些代码
l
好处
l
关于代码
l
ToDo
l
历史
引言
C/C++
的内存分配
(
通过
malloc
或
new)
可能需要花费很多时。
更糟糕的是,随着时间的流逝,内存
(memory)
将形成碎片,所以一个应用程序的运行会越来越慢当它运行了很长时间和
/
或执行了很多的内存分配
(
释放
)
操作的时候。特别是,你经常申请很小的一块内存,堆
(heap)
会变成碎片的。
解决方案:你自己的内存池
一个
(
可能的
)
解决方法是内存池
(Memory Pool)
。
在启动的时候,一个
”
内存池
”(Memory Pool)
分配一块很大的内存,并将会将这个大块
(block)
分成较小的块
(smaller chunks)
。每次你从内存池申请内存空间时,它会从先前已经分配的块
(chunks)
中得到,
而不是从操作系统。最大的优势在于:
l
非常少
(
几没有
)
堆碎片
l
比通常的内存申请
/
释放
(
比如通过
malloc
,
new
等
)
的方式快
另外,你可以得到以下好处:
l
检查任何一个指针是否在内存池里
l
写一个
”
堆转储
(
Heap-Dump
)”
到你的硬盘
(
对事后的调试非常有用
)
l
某种
”
内存泄漏检测
(
memory-leak detection
)”
:当你没有释放所有以前分配的内存时,内存池
(Memory Pool)
会抛出一个断言
(
assertion
).
它怎样工作
让我们看一看内存池
(Memory Pool)
的
UML
模式图:
这个模式图只显示了类
CMemoryPool
的一小部分,参看由
Doxygen
生成的文档以得到详细的类描述。
一个关于内存块(MemoryChunks)的单词
你应该从模式图中看到,
内存池
(Memory Pool)
管理了一个指向结构体
SMemoryChunk
(
m_ptrFirstChunk
,
m_ptrLastChunk
, and
m_ptrCursorChunk
)
的指针。这些块
(chunks)
建立一个内存块
(memory chunks)
的链表。各自指向链表中的下一个块
(chunk)
。当从操作系统分配到一块内存时,它将完全的被
SMemoryChunk
s
管理。让我们近一点看看一个块
(chunk)
。
typedef struct
SMemoryChunk
...
{
TByte *Data ; // The actual Data
std::size_t DataSize ; // Size of the "Data"-Block
std::size_t UsedSize ; // actual used Size
bool IsAllocationChunk ; // true, when this MemoryChunks
// Points to a "Data"-Block
// which can be deallocated via "free()"
SMemoryChunk *Next ; // Pointer to the Next MemoryChunk
// in the List (may be NULL)
}
SmemoryChunk;
每个块(chunk)持有一个指针,指针指向:
l
一小块内存
(
Data
)
,
l
从块
(chunk)
开始的可用内存的总大小
(
DataSize
)
,
l
实际使用的大小
(
UsedSize
)
,
l
以及一个指向链表中下一个块
(chunk)
的指针。
第一步:预申请内存(pre-allocating the memory)
当你调用
CmemoryPool
的构造函数,内存池
(Memory Pool)
将从操作系统申请它的第一块
(
大的
)
内存块
(memory-chunk)
/**/
/*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
最终完成了从操作系统申请内存。
/**/
/******************
AllocateMemory
******************/
bool
CMemoryPool::AllocateMemory(
const
std::size_t
&
sMemorySize)
...
{
std::size_t sBestMemBlockSize = CalculateBestMemoryBlockSize(sMemorySize) ;
// allocate from Operating System
TByte *ptrNewMemBlock = (TByte *) malloc (sBestMemBlockSize) ;
...
那么,是如何管理数据的呢?
第二步:已分配内存的分割(segmentation of allocated memory)
正如前面提到的,
内存池(
Memory Pool
)
使用
SMemoryChunk
s
管理所有数据。从OS申请完内存之后,我们的块(chunks)和实际的内存块(block)之间就不存在联系:
Memory Pool after initial allocation
我们需要分配一个结构体
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()
负责计算为管理已经得到的内存需要的块(chunks)的数量。分配完块(chunks)之后(通过
malloc
)
,
ptrNewChunks
将指向一个
SmemoryChunk
s
的数组。注意,数组里的块
(chunks)
现在持有的是垃圾数据,因为我们还没有给
chunk-members
赋有用的数据。内存池的堆
(Memory Pool-"Heap"):
Memory Pool after
SMemoryChunk
allocation
还是那句话,数据块
(data block)
和
chunks
之间没有联系。但是,
AllocateMemory()
会照顾它。
LinkChunksToData()
最后将把数据块
(data block)
和
chunks
联系起来,并将为每个
chunk-member
赋一个可用的值。
//
(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]) ;
// The first Chunk assigned to the new Memory-Block will be
// a "AllocationChunk". This means, this Chunks stores the
// "original" Pointer to the MemBlock and is responsible for
// "free()"ing the Memory later....
if(!bAllocationChunkAssigned)
...{
m_ptrLastChunk->IsAllocationChunk = true ;
bAllocationChunkAssigned = true ;
}
}
return RecalcChunkMemorySize(m_ptrFirstChunk, m_uiMemoryChunkCount) ;
}
让我们一步步地仔细看看这个重要的函数:第一行检查链表里是否已经有可用的块(chunks):
...
if
(
!
m_ptrFirstChunk)
...
我们第一次给类的成员赋值:
...
m_ptrFirstChunk
=
SetChunkDefaults(
&
(ptrNewChunks[
0
])) ;
m_ptrLastChunk
=
m_ptrFirstChunk ;
m_ptrCursorChunk
=
m_ptrFirstChunk ;
...
m_ptrFirstChunk
现在指向块数组(
chunks-array
)
的第一个块,每一个块严格的管理来自内存(
memory block
)
的
m_sMemoryChunkSize
个字节。一个
”
偏移量
”(offset)
——这个值是可以计算的所以每个
(chunk)
能够指向内存块
(
memory block)
的特定部分。
uiMemOffSet
=
(i
*
((unsigned
int
) m_sMemoryChunkSize)) ;
m_ptrLastChunk
->
Data
=
&
(ptrNewMemBlock[uiMemOffSet]) ;
另外,每个新的来自数组的
SmemoryChunk
将被追加到链表的最后一个元素(并且它自己将成为最后一个元素):
...
m_ptrLastChunk
->
Next
=
ptrNewChunk ;
m_ptrLastChunk
=
ptrNewChunk ;
...
在接下来的
"
for loop" 中,内存池(memory pool)将连续的给数组中的所有块(chunks)赋一个可用的数据。
Memory and chunks linked together, pointing to valid data
最后,我们必须重新计算每个块(chunk)能够管理的总的内存大小。这是一个费时的,但是在新的内存追加到内存池时必须做的一件事。这个总的大小将被赋值给chunk的
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
之后,每个chunk都知道它指向的空闲内存的大小。所以,将很容易确定一个chunk是否能够持有一块特定大小的内存:当
DataSize
成员大于
(
或等于
)
已经申请的内存大小以及
DataSize
成员是
0
,于是
chunk
有能力持有一块内存。最后,内存分割完成了。为了不让事情太抽象,我们假定内存池
(memory pool )
包含600字节,每个chunk持有100字节。
Memory segmentation finished. Each chunk manages exactly 100 bytes
第三步:从内存池申请内存(requesting memory from the memory pool)
那么,如果用户从内存池申请内存会发生什么?最初,内存池里的所有数据是空闲的可用的:
All memory blocks are available
我们看看
GetMemory
:
/**/
/******************
GetMemory
******************/
void
*
CMemoryPool::GetMemory(
const
std::size_t
&
sMemorySize)
...
{
std::size_t sBestMemBlockSize = CalculateBestMemoryBlockSize(sMemorySize) ;
SMemoryChunk *ptrChunk = NULL ;
while(!ptrChunk)
...{
// Is a Chunks available to hold the requested amount of Memory ?
ptrChunk = FindChunkSuitableToHoldMemory(sBestMemBlockSize) ;
if (!ptrChunk)
...{
// No chunk can be found
// => Memory-Pool is to small. We have to request
// more Memory from the Operating-System....
sBestMemBlockSize = MaxValue(sBestMemBlockSize,
CalculateBestMemoryBlockSize(m_sMinimalMemorySizeToAllocate)) ;
AllocateMemory(sBestMemBlockSize) ;
}
}
// Finally, a suitable Chunk was found.
// Adjust the Values of the internal "TotalSize"/"UsedSize" Members and
// the Values of the MemoryChunk itself.
m_sUsedMemoryPoolSize += sBestMemBlockSize ;
m_sFreeMemoryPoolSize -= sBestMemBlockSize ;
m_uiObjectCount++ ;
SetMemoryChunkValues(ptrChunk, sBestMemBlockSize) ;
// eventually, return the Pointer to the User
return ((void *) ptrChunk->Data) ;
}
当用户从内存池中申请内存是,它将从链表搜索一个能够持有被申请大小的chunk。那意味着:
l
那个chunk的
DataSize
必须大于或等于被申请的内存的大小;
l
那个chunk的
UsedSize
必须是
0
。
这由
FindChunkSuitableToHoldMemory
方法完成。如果它返回
NULL
,那么在内存池中没有可用的内存。这将导致
AllocateMemory
的调用
(
上面讨论过
)
,它将从
OS
申请更多的内存。如果返回值不是
NULL
,
一个可用的
chunk
被发现。
SetMemoryChunkValues
会调整
chunk
成员的值,并且最后
Data
指针被返回给用户
...
/**/
/******************
SetMemoryChunkValues
******************/
void
CMemoryPool::SetMemoryChunkValues(SMemoryChunk
*
ptrChunk,
const
std::size_t
&
sMemBlockSize)
...
{
if(ptrChunk)
...{
ptrChunk->UsedSize = sMemBlockSize ;
}
...
}
示例
假设,用户从内存池申请
250
字节:
Memory in use
如我们所见,每个内存块(chunk)管理100字节,所以在这里250字节不是很合适。发生了什么事?Well,
GetMemory
从第一个chunk返回
Data
指针并把它的
UsedSize
设为300字节,因为300字节是能够被管理的内存的最小值并大于等于250。那些剩下的
(300 - 250 = 50)
字节被称为内存池的
"memory overhead"
。这没有看起来的那么坏,因为这些内存还可以使用
(
它仍然在内存池里
)
。
当
FindChunkSuitableToHoldMemory
搜索可用
chunk
时,它仅仅从一个空的
chunk
跳到另一个空的
chunk
。那意味着,如果某个人申请另一块内存
(memory-chunk)
,第四块
(
持有
300
字节的那个
)
会成为下一个可用的
("valid") chunk
。
Jump to next valid chunk
使用代码
使用这些代码是简单的、直截了当的:只需要在你的应用里包含
"CMemoryPool.h"
,并添加几个相关的文件到你的
IDE/Makefile:
- CMemoryPool.h
- CMemoryPool.cpp
- IMemoryBlock.h
- SMemoryChunk.h
你只要创建一个
CmemoryPool
类的实例,你就可以从它里面申请内存。所有的内存池的配置在
CmemoryPool
类的构造函数
(
使用可选的参数
)
里完成。看一看头文件
("CMemoryPool.h")
或
Doxygen-doku
。所有的文件都有详细的
(Doxygen-)
文档。
应用举例
MemPool::CMemoryPool
*
g_ptrMemPool
=
new
MemPool::CMemoryPool() ;
char
*
ptrCharArray
=
(
char
*
) g_ptrMemPool
->
GetMemory(
100
) ;
...
g_ptrMemPool
->
FreeMemory(ptrCharArray,
100
) ;
delete g_ptrMemPool ;
好处
内存转储(Memory dump)
你可以在任何时候通过
WriteMemoryDumpToFile(strFileName)
写一个
"memory dump"
到你的
HDD
。看看一个简单的测试类的构造函数
(
使用内存池重载了
new
和
delete
运算符
)
:
/**/
/******************
Constructor
******************/
MyTestClass::MyTestClass()
...
{
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
"
) ;
看一看内存转储文件
("MemoryDump.bin"):
如你所见,在内存转储里有
MyTestClass
类的所有成员的值。明显的,
"Hello"
字符串
(
m_cMyArray
)
在那里,以及整型数
m_iMyInt
(3930 0000 = 0x3039 = 12345 decimal)
等等。这对调式很有用。
速度测试
我在
Windows
平台上做了几个非常简单的测试
(
通过
timeGetTime()
)
,但是结果说明内存池大大提高了应用程序的速度。所有的测试在
Microsoft Visual Studio .NET 2003
的
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) ;
}
Results for the "array-test
//Class-Test for MemoryPool and Heap (overloaded new/delete)
//
Class-Test for MemoryPool and Heap (overloaded new/delete)
for
(unsigned
int
j
=
0
; j
<
TestCount; j
++
)
...
{
MyTestClass *ptrTestClass = new MyTestClass ;
delete ptrTestClass ;
}
Results for the "classes-test" (overloaded
new/delete operators)
关于代码
这些代码在Windows和Linux平台的下列编译器测试通过:
- Microsoft Visual C++ 6.0
- Microsoft Visual C++ .NET 2003
- MinGW (GCC) 3.4.4 (Windows)
- GCC 4.0.X (Debian GNU Linux)
Microsoft Visual C++ 6.0(*.dsw, *.dsp)
和
Microsoft Visual C++ .NET 2003 (*.sln, *.vcproj)
的工程文件已经包含在下载中。内存池仅用于
ANSI/ISO C++,
所以它应当在任何
OS
上的标准的
C++
编译器编译。在
64
位处理器上应当没有问题。
注意
:内存池不是线程安全的。
ToDo
这个内存池还有许多改进的地方
;-) ToDo
列表包括:
l
对于大量的内存,
memory-"overhead"
能够足够大。
l
某些
CalculateNeededChunks
调用能够通过从新设计某些方法而去掉
l
更多的稳定性测试
(
特别是对于那些长期运行的应用程序
)
l
做到线程安全。
历史
l
05.09.2006: Initial release
EoF
DanDanger2000