序言html
最近在网上看到了几篇篇讲述内存池技术的文章,有一篇是有IBM中国研发中心的人写的,写的不错~~文章地址在本篇blog最后。原文的讲述比个人要清晰不少,我在这只是把个人一些理解和遇到的一些问题和你们分享一下~~linux
1、为何要使用内存池技术呢c++
主要有两个缘由:一、减小new、delete次数,减小运行时间;二、避免内存碎片。算法
一、效率数组
c语言中使用malloc/free来分配内存,c++中使用new/delete来分配内存,他们的内存申请与释放都是与操做系统进行交互的。具体的内容在严蔚敏数据结构的第八章有相关讲述,主要就是系统要维护一个内存链表,当有一个内存申请过来时,根据相应的分配算法在链表中找个一个合适的内存分配给它。这些算法有的是分配最早找到的不小于申请内存的内存块,有的是分配最大的内存块,有的是分配最接近申请内存大小的内存块。分配的内存块可能会大于所申请的内存大小,这样还有进行切割,将剩余的内存插入到空闲链表中。当释放的时候,系统可能要对内存进行整理,判断free的内存块的先后是否有空闲,如有的话还要进行合并。此外,new/delete还要考虑多线程的状况。总之一句话,调用库中的内存分配函数,十分的耗时~~数据结构
二、内存碎片多线程
什么是内存碎片内,从字面意思就很好理解了,就是内存再也不是一整块的了,而是碎了。由于连续的这种new/delete操做,一大块内存肯能就被分割成小的内存分配出去了,这些小的内存都是不连续的。当你再去分配大的连续内存的时候,尽管剩余内存的总和可能大于所要分配的内存大小,但系统就找不到连续的内存了,因此致使分配错误。malloc的时候会致使返回NULL,而new的时候再vc6.0中返回NULL,vs2003以上则是抛出异常。函数
2、原理post
要解决上述两个问题,最好的方法就是内存池技术。具体方法就是大小固定、提早申请、重复利用。url
由于内存的申请和释放是很低效的,因此咱们只在开始时申请一块大的内存(在该块内存不够用时在二次分配),而后每次须要时都从这块内存中取出,并标记下这块内存被用了,释放时标记此内存被释放了。释放时,并不真的把内存释放给操做系统,只要在一大块内存都空闲的时候,才释放给操做系统。这样,就减小了new/delete的操做次数,从而提升了效率。
在调用内存分配函数的时候,大部分时间所分配的内存大小都是必定的,因此能够采用每次都分配固定大小的内存块,这样就避免了内存碎片产生的可能。
3、具体实现
我所采用的内存池的构造方法彻底是按照文章1所介绍的方法,内存池的结构图以下:
如图所示MemoryPool是一个内存池类,其中pBlock是一个指向了一个内存块的指针,nUintSzie是分配单元的大小,nInitSize是第一次分配时向系统申请的内存的大小,nGrouSize是后面每次向系统申请的内存的大小。
MemoryBloc表明一个内存块单元,它有两部分构成,一部分时MemoryBlock类的大小,另外一部分则是实际的内存部分。一个MemoryBlock的内存是在重载的new操做符中分配的,以下所示:
void
* MemoryBlock::operator
new
(
size_t
,
int
nUnitSize,
int
nUnitAmount )
{
return
::operator
new
(
sizeof
(MemoryBlock) + nUnitSize * nUnitAmount );
}
|
MemoryBlock内中,nSize代码该内存块的大小(系统分配内存大小-MemoryBlock类的大小),nFree是空闲内存单元的个数,nFirst表明的是下一个要分配的内存单元的序号。aData是用来记录待分配内存的位置的。由于要分配的内存是在new中一块儿向系统申请的,并无一个指针指向这块内存的位置,但它的位置就在MemoryBlock这个类的地址开始的,因此能够用MemoryBlock的最后一个成员的位置来表示待分配内存的位置。
带分配内存中,是以nUnitSize为单位的,一个内存单元的头两个字节都记录了下一个要分配的内存单元的序号,序号从0开始。这样实际也就构成了一个数组链表。由MemoryBlock的构造函数来完成这个链表的初始化工做:
MemoryBlock::MemoryBlock(
int
nUnitSize,
int
nUnitAmount )
: nSize (nUnitAmount * nUnitSize),
nFree (nUnitAmount - 1),
//构造的时候,就已将第一个单元分配出去了,因此减一
nFirst (1),
//同上
pNext (NULL)
{
//初始化数组链表,将每一个分配单元的下一个分配单元的序号写在当前单元的前两个字节中
char
* pData = aData;
//最后一个位置不用写入
for
(
int
i = 1; i < nSize - 1; i++)
{
(*(
USHORT
*)pData) = i;
pData += nUnitSize;
}
}
|
在MemoryPool的Alloc()中,遍历block链表,找到nFree大于0的block,从其上分配内存单元。而后将nFree减一,修改nFirst的值。
在MemoryPool的Free(pFree)函数中,根据pFree的值,找到它所在的内存块,而后将它的序号做为nFirst的值(由于它绝对是空闲的),在pFree的头两个字节中写入原来nFirst的值。而后要判断,该block是否所有为free,方法是检测nFree * nUnitSize == nSize。如果,则向系统释放内存,若不是,则将该block放到链表的头部,由于该block上必定含有空隙的内存单元,这样能够减小分配时遍历链表所消耗的时间。
4、使用
内存池通常都是做为一个类的静态成员,或者全局变量。使用时,重载new操做符,使其到MemoryPool中去分配内存,而不是向系统申请。这样,一个类的因此对象都在一个内存池中开辟空间。
void
CTest::operator
delete
(
void
* pTest )
{
Pool.Free(pTest);
}
void
* CTest::operator
new
(
size_t
)
{
return
(CTest*)Pool.Alloc();
}
|
5、代码
MemoryPool.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
#include <stdlib.h>
#include <wtypes.h>
#define MEMPOOL_ALIGNMENT 8 //对齐长度
//内存块,每一个内存块管理一大块内存,包括许多分配单元
class
MemoryBlock
{
public
:
MemoryBlock (
int
nUnitSize,
int
nUnitAmount);
~MemoryBlock(){};
static
void
* operator
new
(
size_t
,
int
nUnitSize,
int
nUnitAmount);
static
void
operator
delete
(
void
* ,
int
nUnitSize,
int
nUnitAmount){};
static
void
operator
delete
(
void
* pBlock);
int
nSize;
//该内存块的大小,以字节为单位
int
nFree;
//该内存块还有多少可分配的单元
int
nFirst;
//当前可用单元的序号,从0开始
MemoryBlock* pNext;
//指向下一个内存块
char
aData[1];
//用于标记分配单元开始的位置,分配单元从aData的位置开始
};
class
MemoryPool
{
public
:
MemoryPool (
int
_nUnitSize,
int
_nGrowSize = 1024,
int
_nInitSzie = 256);
~MemoryPool();
void
* Alloc();
void
Free(
void
* pFree);
private
:
int
nInitSize;
//初始大小
int
nGrowSize;
//增加大小
int
nUnitSize;
//分配单元大小
MemoryBlock* pBlock;
//内存块链表
};
|
MemoryPool.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
|
#include "MemoryPool.h"
MemoryBlock::MemoryBlock(
int
nUnitSize,
int
nUnitAmount )
: nSize (nUnitAmount * nUnitSize),
nFree (nUnitAmount - 1),
//构造的时候,就已将第一个单元分配出去了,因此减一
nFirst (1),
//同上
pNext (NULL)
{
//初始化数组链表,将每一个分配单元的下一个分配单元的序号写在当前单元的前两个字节中
char
* pData = aData;
//最后一个位置不用写入
for
(
int
i = 1; i < nSize - 1; i++)
{
(*(
USHORT
*)pData) = i;
pData += nUnitSize;
}
}
void
* MemoryBlock::operator
new
(
size_t
,
int
nUnitSize,
int
nUnitAmount )
{
return
::operator
new
(
sizeof
(MemoryBlock) + nUnitSize * nUnitAmount );
}
void
MemoryBlock::operator
delete
(
void
* pBlock)
{
::operator
delete
(pBlock);
}
MemoryPool::MemoryPool(
int
_nUnitSize,
int
_nGrowSize
/*= 1024*/
,
int
_nInitSzie
/*= 256*/
)
{
nInitSize = _nInitSzie;
nGrowSize = _nGrowSize;
pBlock = NULL;
if
(_nUnitSize > 4)
nUnitSize = (_nUnitSize + (MEMPOOL_ALIGNMENT - 1)) & ~(MEMPOOL_ALIGNMENT - 1);
else
if
( _nUnitSize < 2)
nUnitSize = 2;
else
nUnitSize = 4;
}
MemoryPool::~MemoryPool()
{
MemoryBlock* pMyBlock = pBlock;
while
( pMyBlock != NULL)
{
pMyBlock = pMyBlock->pNext;
delete
(pMyBlock);
}
}
void
* MemoryPool::Alloc()
{
if
( NULL == pBlock)
{
//首次生成MemoryBlock,new带参数,new了一个MemoryBlock类
pBlock = (MemoryBlock*)
new
(nUnitSize,nInitSize) MemoryBlock(nUnitSize,nUnitSize);
return
(
void
*)pBlock->aData;
}
//找到符合条件的内存块
MemoryBlock* pMyBlock = pBlock;
while
( pMyBlock != NULL && 0 == pMyBlock->nFree )
pMyBlock = pMyBlock->pNext;
if
( pMyBlock != NULL)
{
//找到了,进行分配
char
* pFree = pMyBlock->aData + pMyBlock->nFirst * nUnitSize;
pMyBlock->nFirst = *((
USHORT
*)pFree);
pMyBlock->nFree--;
return
(
void
*)pFree;
}
else
{
//没有找到,说明原来的内存块都满了,要再次分配
if
( 0 == nGrowSize)
return
NULL;
pMyBlock = (MemoryBlock*)
new
(nUnitSize,nGrowSize) MemoryBlock(nUnitSize,nGrowSize);
if
( NULL == pMyBlock)
return
NULL;
//进行一次插入操做
pMyBlock->pNext = pBlock;
pBlock = pMyBlock;
return
(
void
*)pMyBlock->aData;
}
}
void
MemoryPool::Free(
void
* pFree )
{
//找到p所在的内存块
MemoryBlock* pMyBlock = pBlock;
MemoryBlock* PreBlock = NULL;
while
( pMyBlock != NULL && ( pBlock->aData > pFree || pMyBlock->aData + pMyBlock->nSize))
{
PreBlock = pMyBlock;
pMyBlock = pMyBlock->pNext;
}
if
( NULL != pMyBlock )
//该内存在本内存池中pMyBlock所指向的内存块中
{
//Step1 修改数组链表
*((
USHORT
*)pFree) = pMyBlock->nFirst;
pMyBlock->nFirst = (
USHORT
)((
ULONG
)pFree - (
ULONG
)pMyBlock->aData) / nUnitSize;
pMyBlock->nFree++;
//Step2 判断是否须要向OS释放内存
if
( pMyBlock->nSize == pMyBlock->nFree * nUnitSize )
{
//在链表中删除该block
delete
(pMyBlock);
}
else
{
//将该block插入到队首
PreBlock = pMyBlock->pNext;
pMyBlock->pNext = pBlock;
pBlock = pMyBlock;
}
}
}
|
CTest.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
#include <stdio.h>
#include "MemoryPool.h"
class
CTest
{
public
:
CTest(){data1 = data2 = 0;};
~CTest(){};
void
* operator
new
(
size_t
);
void
operator
delete
(
void
* pTest);
public
:
static
MemoryPool Pool;
int
data1;
int
data2;
};
void
CTest::operator
delete
(
void
* pTest )
{
Pool.Free(pTest);
}
void
* CTest::operator
new
(
size_t
)
{
return
(CTest*)Pool.Alloc();
}
MemoryPool CTest::Pool(
sizeof
(CTest));
int
main()
{
CTest* pTest =
new
CTest;
printf
(
"%d"
,pTest->data2);
}
|
6、问题
在编写代码时,遇到了一些小问题,现与你们分享以下:
一、重载new操做符时,编译器要求是第一个参数必须是size_t,返回值必须是void*;free的第一个参数必须是void*.
二、通常要在类的成员中重载new操做符,而不要重载全局的new操做符。
三、一个类中要是重载了一个new操做符,必定要有一个相应类型的delete操做符,能够什么都不干,但必须有,不然在构造函数失败时,找不到对应的delete函数。
例如:
1
2
|
static
void
* operator
new
(
size_t
,
int
nUnitSize,
int
nUnitAmount);
static
void
operator
delete
(
void
* ,
int
nUnitSize,
int
nUnitAmount){};
|
四、带参数的new操做符
pBlock = (MemoryBlock*)
new
(nUnitSize,nInitSize) MemoryBlock(nUnitSize,nUnitSize);
|
第一个nUnitSize nInitSize是new操做符的参数,该new操做符是new了一个MemoryBlock对象,在new返回的地址上构造MemoryBlock的对象。
五、若是在类的内部不能进行静态成员的定义的话,能够只在内部进行声明,在外部定义:
MemoryPool CTest::Pool(
sizeof
(CTest));
|
------------------------------------------------------------END----------------------------------------------------------------------
文章1:http://www.ibm.com/developerworks/cn/linux/l-cn-ppp/index6.html;
文章2:http://www.codeproject.com/Articles/27487/Why-to-use-memory-pool-and-how-to-implement-it;