序言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;