一、内存池
内存池是一种动态内存分配与管理技术,程序员习惯直接使用new、delete、free、malloc等这些API申请和释放内存,这样导致的后果:由于申请的大小不固定,频繁的使用会造成内存碎片从而降低程序和操作系统的性能。内存池则是在使用时,先申请一大块内存留做备用,当程序需要使用时,就直接使用内存池中的内存,当程序要将内存释放时,就先释放到内存池中。当再次申请时,就再次从内存池中取出使用。这样可以避免频繁的找系统申请,大大加快了系统的性能。
1.1、好处
- 1、我们一次性向系统申请一大块内存,这样可以降低内存碎片问题。
- 2、当我们已经使用过的内存释放过后就放入内存池中,第二次再次申请时我们直接往内存池取出内存,这样提高了内存的分配率。
1.2、定成内存池
下面我介绍的内存池是针对某种固定的对象使用的。但是原理和上面是一样的:一次性向系统申请一大块内存,然后将大块内存分别切割为对象大小的内存分配给对象使用。当这个对象释放后,我们将对象还回来的内存放到内存池中。下次要申请内存时直接从内存池中获取。
1.3、内存池的申请
我们使用链表把通过malloc一次性申请的内存管理起来,最后通过析构函数时,只需要释放链表即可。
1.4、自由链表的释放与管理
1.5、技巧
为了维护链表,每个free-list节点需要额外的指针(指向下一个节点),这不仅又造成另一种额外的负担吗?但是我们可以通过将对象的前4个字节或者8个字节用来做存区下一个指针的地址,就可以解决了。
void* free_list;
T* obj;
//32位系统下:
//将对象放回内存池
*((int*)obj)=(int)free_list;
free_list=obj;
//从内存池中取内存
obj=(T*)free_list;
free_list=*((int*)obj);
//64位系统下:
//将对象放回内存池
*((long*)obj)=(long)free_list;
free_list=obj;
//从内存池中取内存
obj=(T*)free_list;
free_list=*((long*)obj);
指针大小由于系统不同而占用的字节数是不同的,我们只能通过判断指针的大小来取对象的前多少个字节?没有其他方法?
void* free_list;
T* obj;
//将对象放回内存池
*((T**)obj)=(T)free_list;
free_list=obj;
//从内存池中取内存
obj=(T*)free_list;
free_list=*((T**)obj);
那么我们将对象转换为二级指针,这样我们在解引用时,指针是多大就解引用指针大小个字节的数,这样我们就解决了系统的不同带来的指针大小不同的问题。
但是如果当对象的大小小于对象指针大小的时候,也就是一个对象的空间存不下一个指针的大小。
例如:为char类型对象开辟内存池,sizeof(char)<sizeof(char*),这时候我们就要为一个char类型对象申请sizeof(char*)大小的内存。
二、内存池的设计步骤
1、初始化
在创建内存池的时候为内存池分配一大块初始内存,便于以后的使用。
2、分配内存
当需要内存的时候就去内存池里面分配内存。先查看自由链表中有无内存,如果没有就把大块内存进行切割为所需要内存大小。当大块内存切割完之后,重新申请,每次申请的内存是上一次申请的某倍数。如果自由链表中有内存就直接取自由链表中的内存。
3、回收内存
将内存挂到自由链表上。
4、释放内存池
将管理大块内存的链表依次释放。不用去管自由链表上的内存块。
C++代码实现
template<class T>
class ObjectPool {
struct BlockNode {
char* memery; //记录当前大块内存的首地址
BlockNode* next; //连接下一块大块内存
size_t objNum; //当前大块内存最多能切出多少个对象
BlockNode(size_t _objNum)
:next(nullptr), objNum(_objNum)
{
memery = (char*)malloc(objNum*itemSize);
}
~BlockNode() {
free(memery);
memery = nullptr;
next = nullptr;
objNum = 0;
}
};
public:
ObjectPool(size_t initNum = 32, size_t _maxNum = 10000)
:curCount(0), maxNum(_maxNum), freelists(nullptr)
{
head = new BlockNode(initNum);
}
~ObjectPool() {
Destory();
}
//根据系统取对象前指针大小个字节
T*& Next(T* obj) {
return *((T**)obj);
}
void Destory() {
while (head) { //释放所有通过malloc申请的大块内存
BlockNode* cur = head;
head = head->next;
delete cur;
}
head = nullptr;
}
void Delete(T* obj) {
obj->~T(); //调用对象的析构函数
Next(obj) = freelists; //通过头插将对象和自由链表连接起来
freelists = obj;
}
T* New() {
T* obj = nullptr;
if (freelists) { //判断自由链表中是否有内存
obj = freelists; //从自由链表中取内存
freelists = Next(freelists); //自由链表移动到下一个自由链表
new(obj)T; //定位new 初始化对象
return obj;
}
if (curCount >= head->objNum) { //当前大块内存已被全部切割出去需要重新分配一个更大的内存
size_t size = curCount * 2; //每次分配是原来的2倍
if (size > maxNum) { //当申请内存达到上限值时,就为上限值大小
size = maxNum;
}
BlockNode* temp = new BlockNode(size);//重新申请大块内存
temp->next = head; //将申请的大块内存通过头插,插入到管理大块内存的链表中
head = temp; //头指针指向新申请的大块内存
curCount = 0;
}
obj = (T*)(head->memery + itemSize * curCount); //将大块内存切割为对象大小的内存
curCount++; //将当前大块内存分配分配出来的小块内存+1
new(obj)T; //定位new 调用对象的构造函数初始化对象
return obj;
}
protected:
size_t curCount; //当前BlockNode已经分配了多少个对象
size_t maxNum; //BlockNode申请是以倍数增长的,这个变量限制最大能申请的内存大小,最大申请内存的上限值
BlockNode* head;//大块内存的链表头
T* freelists; //自由链表
static size_t itemSize; //对象的大小;当对象的大小小于指针时,就取指针的大小,当指针大小小于对象大小时就取对象大小
static size_t GetItemSize() {
return sizeof(T) > sizeof(T*) ? sizeof(T):sizeof(T*);
}
};
template<class T>
size_t ObjectPool<T>::itemSize = ObjectPool<T>::GetItemSize();
三、定长内存池的测试
void TestObjectPool() {
size_t begin1 = clock();
std::vector<char*> v1;
for (int i = 0; i < 100000; ++i)
{
v1.push_back(new char);
}
for (int i = 0; i < 100000; ++i)
{
delete v1[i];
}
v1.clear();
for (int i = 0; i < 100000; ++i)
{
v1.push_back(new char);
}
for (int i = 0; i < 100000; ++i)
{
delete v1[i];
}
v1.clear();
size_t end1 = clock();
ObjectPool<char> tnPool;
size_t begin2 = clock();
std::vector<char*> v2;
for (int i = 0; i < 100000; ++i)
{
v2.push_back(tnPool.New());
}
for (int i = 0; i < 100000; ++i)
{
tnPool.Delete(v2[i]);
}
v2.clear();
for (int i = 0; i < 100000; ++i)
{
v2.push_back(tnPool.New());
}
for (int i = 0; i < 100000; ++i)
{
tnPool.Delete(v2[i]);
}
v2.clear();
size_t end2 = clock();
cout <<"malloc函数2轮100000次申请和释放所用时间:" <<end1 - begin1 <<"ms"<< endl;
cout << "定成内存池2轮100000次申请和释放所用时间:"<<end2 - begin2 << "ms"<<endl;
}
int main() {
TestObjectPool();
return 0;
}
通过测试我们还是能看出内存池的效率大大提高了。