单线程环境中对象大小固定的内存池

内存池并不是表面意义上存储大量可分配内存的池子,如果是这样的话,new和delete对应的就是一个理论上无限大的内存池。标题中的单线程指的是该内存池实现为了加快速度没有处理线程同步,因为很多应用你明显知道不需要线程同步。固定大小指的是该内存池每次只分配或者删除固定大小的对象。

这样的内存池怎么就加快了速度了。我觉得主要有以下几个方面。

1.不需要处理线程同步,去除掉线程同步的代码后,显然速度会加快。

2.总体来看,该内存池只会扩张不会收缩,也就是向系统释放堆内存的次数减少了。事实上,该内存池总是按需操作的。

3.该内存池的new和delete操作并没有去真正申请和释放堆内存,而是向自定义的对象空闲列表申请和释放。当对象空闲列表为空的时候,才一次性申请一大片空闲对象。大家都知道堆申请和释放比较耗时,最快的栈内存。所以,这也是一个显著的方面。

以下2段代码分别展示了原始的内存操作和使用内存池后的操作,

原始情况,

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
#include
#include

class Rational
{
public:
Rational(int a = 0, int b = 1) : n(a), d(b) {}

private:
int n; // Numerator
int d; // Denominator
};

int main()
{
const int ARRAY_SIZE = 1000;
const int LOOP_TIMES = 5000;
Rational* array[ARRAY_SIZE];

clock_t beg = clock();
for (int i = 0; i < LOOP_TIMES; ++i)
{
for (int j = 0; j < ARRAY_SIZE; ++j)
{
array[j] = new Rational(j);
}
for (int j = 0; j < ARRAY_SIZE; ++j)
{
delete array[j];
}
}
clock_t end = clock();
printf("use %f second(s).\n", 1.0 * (end - beg) / CLOCKS_PER_SEC);
system("pause");

return 0;
}

该代码的运行结果,

内存池,

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
#include <stdio.h>
#include <time.h>
#include <stdlib.h>

struct NextOnFreeList
{
NextOnFreeList* next;
};

class Rational
{
public:
Rational(int a = 0, int b = 1) : n(a), d(b) {}

void* operator new(size_t size)
{
if (freeList == 0)// if the list is empty, fill it up.
{
expandTheFreeList();
}

NextOnFreeList* head = freeList;
freeList = head->next;
return head;
}

void operator delete(void* doomed, size_t size)
{
NextOnFreeList* head = static_cast<NextOnFreeList*> (doomed);
head->next = freeList;
freeList = head;
}
static void newMemPool()
{
expandTheFreeList();
}
static void deleteMemPool();

private:
static void expandTheFreeList();
static NextOnFreeList* freeList;
enum { EXPANSION_SIZE = 32 };
int n; // Numerator
int d; // Denominator
};

NextOnFreeList* Rational::freeList = NULL;

//当空闲列表里面没有对象时,该函数才会被调用
void Rational::expandTheFreeList()
{
size_t size = sizeof(Rational) > sizeof(NextOnFreeList*) ?
sizeof(Rational) : sizeof(NextOnFreeList*);
NextOnFreeList* runner = (NextOnFreeList*)new char[size];
freeList = runner;
for (int i = 0; i < EXPANSION_SIZE; ++i)
{
runner->next = (NextOnFreeList*)new char[size];
runner = runner->next;
}
runner->next = 0;
}

void Rational::deleteMemPool()
{
NextOnFreeList* nextPtr = freeList;
while (nextPtr)
{
freeList = nextPtr->next;
delete [] nextPtr;
nextPtr = freeList;
}
}

int main()
{
const int ARRAY_SIZE = 1000;
const int LOOP_TIMES = 5000;
Rational* array[ARRAY_SIZE];

clock_t beg = clock();
Rational::newMemPool();
for (int i = 0; i < LOOP_TIMES; ++i)
{
for (int j = 0; j < ARRAY_SIZE; ++j)
{
array[j] = new Rational(j);
}
for (int j = 0; j < ARRAY_SIZE; ++j)
{
delete array[j];
}
}
clock_t end = clock();
printf("use %f second(s).\n", 1.0 * (end - beg) / CLOCKS_PER_SEC);
system("pause");
Rational::deleteMemPool();

return 0;
}

该代码运行结果,

通过以上结果的对比可以看到时间比例为921/78,直接就快了10多倍。而且我们的内存申请和删除操作还不够频繁,如果再更频繁的场合,效率上面的提示会更加大。

下面我们来分析一下内存池实现的代码。

说来也比较简单,因为原理都已经讲清楚了。首先去除了线程同步代码,然后把内存放在对象空闲列表里面,从里面释放和删除对象,当对象空闲列表为空的时候,一次性申请一片对象。作为一个有经验的c++程序员这些代码已经足够简单了。需要注意的是,在expandTheFreeList函数里面,申请新对象的时候,需要注意大小必须是Rational和NextOnFreeList*中大小大的一个。该内存池还用到了指针类型强转,语法上面还是比较巧妙的。

由于该内存池只是针对单个的类,应用场合未免有点限制。如果我们用泛型编程实现内存池,就可以把应用场合推广了。下面将内存池的功能独立出来,用模板重新实现了下,代码如下:

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
#include
#include
#include

template class MemoryPool
{
public:
MemoryPool(size_t size = EXPANSION_SIZE)
{
expandTheFreeList(size);
}
~MemoryPool();
//allocate a T element from the free list.
void* alloc(size_t size)
{
if (next == NULL)
{
expandTheFreeList();
}
MemoryPool* head = next;
next = head->next;
return head;
}
//return a T element to the free list.
void free(void* doomed)
{
MemoryPool* head = static_cast< MemoryPool* > (doomed);
head->next = next;
next = head;
}

private:
//next element on the free list.
MemoryPool* next;
//if the freelist is empty, expand it by this amount.
enum {EXPANSION_SIZE = 32};
//add free elements to the free list
void expandTheFreeList(int howMany = EXPANSION_SIZE);
};

template MemoryPool :: ~MemoryPool()
{
MemoryPool* nextPtr = NULL;
while (nextPtr)
{
nextPtr = next;
next = next->next;
delete [] nextPtr;
}
}

template void MemoryPool :: expandTheFreeList(int howMany)
{
//we must allocate an object enough to contain the next pointer
size_t size = sizeof(T) > sizeof(MemoryPool*) ? sizeof(T) :
sizeof(MemoryPool*);

MemoryPool* runner = (MemoryPool*) new char[size];
next = runner;
for (int i = 0; i < howMany; ++i) { runner->next = (MemoryPool*) new char[size];
runner = runner->next;
}
runner->next = NULL;
}

class Rational
{
public:
Rational(int a = 0, int b = 1) : n(a), d(b) {}
void* operator new(size_t size) {return memPool->alloc(size);}
void operator delete(void* doomed, size_t size) {memPool->free(doomed);}
static void newMemPool() {memPool = new MemoryPool;}
static void deleteMemPool() {delete memPool;}

private:
int n;
int d;
static MemoryPool* memPool;
};
MemoryPool* Rational::memPool = NULL;

int main()
{
const int ARRAY_SIZE = 1000;
const int LOOP_TIMES = 5000;
Rational* array[ARRAY_SIZE];

clock_t beg = clock();
Rational::newMemPool();
for (int i = 0; i < LOOP_TIMES; ++i)
{
for (int j = 0; j < ARRAY_SIZE; ++j)
{
array[j] = new Rational(j);
}
for (int j = 0; j < ARRAY_SIZE; ++j)
{
delete array[j];
}
}
clock_t end = clock();
printf("use %f second(s).\n", 1.0 * (end - beg) / CLOCKS_PER_SEC);
system("pause");
Rational::deleteMemPool();

return 0;
}

运行效果如图:

可以看到用模板重新实现后,速度并没有下降多少,这个例子速度下降近一倍的原因估计是与原来的实现相比多了一些函数调用吧,但是还是比不使用内存池的情况下快一个数量级。

整个代码的思路是把内存池的实现部分用一个模板类包含了,用不同的类实例化模板类,就可以对不同的类使用内存池了,泛型编程在速度和灵活性上面的结合确实是非常优美的。而且你可以看到,例子二和例子三的主函数部分完全一致,也就是保持了外部接口的统一性,只需要修改类Rational的实现即可。现在将内存池应用到不同的类也只需要在类里面添加一个针对该类的泛型内存池成员而已。