定长内存池
- 1.项目介绍
- 2.代码部分
- 3.测试结果
- 4.相关细节分析总结
1.项目介绍
这是一个 C++ 中的对象池(Object Pool)的简单实现,用于更有效地管理对象的内存分配和回收。对象池是一种内存管理技术,旨在减少频繁分配和释放对象的开销,从而提高程序的性能。
- 以下是该对象池的一些关键部分和功能:
New() 函数:用于获取一个新的对象。它首先检查是否有已归还的内存块,如果有,则使用归还的内存,否则从内存池中分配一个新的对象。它使用 malloc 来分配内存,然后使用定位 new 来调用对象的构造函数,初始化对象。
Delete() 函数:用于回收一个对象。它首先调用对象的析构函数以清理对象,然后将对象添加到内存池的归还链表中。为了解决不同对象类型的大小差异问题,它使用了一个二级指针 _freeList,这样不同类型的对象都可以通过 _freeList 连接在一起。
内存分配:初始时,对象池会通过 malloc 分配一块大内存块,大小为1024 * 128字节。然后,通过不断更新 _memory 指针和 _remainBytes 记录内存块的剩余空间,来实现对象的内存分配。
性能测试:在代码中,有一个测试函数 TestObjectPool,用于测试直接使用 new 和对象池分配的性能差异。它执行多轮的分配和释放操作,以测量两种方法的性能。
这个对象池的实现是为了演示目的而创建的,可以在需要管理大量相同类型对象的情况下提高性能
2.代码部分
- ObjectPool.h
#pragma once
#include<vector>
#include<iostream>
#include<time.h>
template <class T>
class ObjectPool
{
public:
T* New()
{
T* obj = nullptr; //给一个对象指针 指向内存
if (_freeList) //如果有归还的内存先用归还内存
{
//next指向freelist+sizeof(T)的一个位置
void* next = static_cast<void*>(static_cast<char*>(_freeList) + sizeof(T));
//void* next = *((void**)_freeList);
obj = (T*)_freeList;
_freeList = next;
return obj;
}
else
{
if (remainBytes < sizeof(T))//如果剩余的内存不够 就新开一块空间
{
_memory = (char*)malloc(1024 * 128); //初次加载内存池创建内存
if (_memory == nullptr)
{
throw std::bad_alloc();//创建失败 抛出异常
}
}
obj = (T*)_memory;
size_t objsize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);//处理后期内存回收时异常
_memory += objsize;//往后移
remainBytes -= objsize;//更新remainBytes
//定位new 用于调用T的构造函数初始话
new(obj) T;
return obj;
}
}
void Delete(T* obj)
{
//显示调用T的析构函数清理对象
obj->~T();
//这里有一个问题 :当T是char类型的时候 大小只有1字节
//在32位系统下 指针是4字节 64位下:8字节
//_freeList = obj; 就会出现异常 所以要用一个二级指针解决这个问题
*(void**)obj = _freeList;
/*当使用二级指针(例如 int** 或 void**)时,它们只是指向另一个指针的指针。
这些指针的大小通常不取决于所指向的数据类型,而是固定的,
因此可以在不同位数的系统上正常工作*/
_freeList = obj;
}
private:
char* _memory = nullptr;//指向大块内存块的地址
size_t remainBytes = 0;//大块内存剩余字节数
void* _freeList = nullptr;//归还回来的内存链表
};
//测试函数
struct TreeNode
{
int _val;
TreeNode* _left;
TreeNode* _right;
TreeNode()
:_val(0)
, _left(nullptr)
, _right(nullptr)
{}
};
void TestObjectPool()
{
//申请释放的轮数
const size_t Rounds = 3;
//每轮申请释放的次数
const size_t N = 1000;
//直接new 测试
size_t begin1 = clock();
std::vector<TreeNode*>v1;
v1.reserve(N);//预留N个空间
for (size_t j = 0; j < Rounds; j++)
{
//插入N个元素
for (size_t i = 0; i < N; ++i)
{
v1.push_back(new TreeNode);//直接new
}
//删除N个元素
for (size_t k = 0; k < N; ++k)
{
delete v1[k];
}
v1.clear();//释放空间
}
size_t end1 = clock();
//用ObjectPool测试
ObjectPool<TreeNode>TNPool;
size_t begin2 = clock();
std::vector<TreeNode*>v2;
v2.reserve(N);
for (size_t j = 0; j < Rounds; j++)
{
//插入N个元素
for (size_t i = 0; i < N; ++i)
{
v2.push_back(TNPool.New());//用objectPool new
}
//删除N个元素
for (size_t k = 0; k < N; ++k)
{
TNPool.Delete(v2[k]);
}
v2.clear();//释放空间
}
size_t end2 = clock();
std::cout << "new const time:" << end1 - begin1 << std::endl;
std::cout << "objectPool const time:" << end2 - begin2 << std::endl;
}
- Test.c
#define _CRT_SECURE_NO_WARNINGS
#include"ObjectPool.h"
int main()
{
TestObjectPool();
return 0;
}
3.测试结果
这里在处理大规模数据的时候效率差距会更明显一些,在这里测试中设置的规模Rounds和N的值设置得比较小(本人设备内存的原因),可以在自己的机器上设置,数据越大,效率差距越明显。
4.相关细节分析总结
- 注意点1
在new()函数这里,这里所说的异常指的是回收内存链表的维护,因为这个链表freelist要存一个指针,
如果这个数据类型是char类型的话大小只有1字节,而指针在32位系统下是4字节,32位系统下是8字节,所以会出现异常。这样处理的话就能巧妙避免这个问题。
- 注意点2
定位new 在处理自定义数据类型的时候很有必要
定位 new:通过使用定位 new,可以在已分配的内存块上调用对象的构造函数,从而正确地初始化对象。这对于确保对象状态的一致性非常重要,尤其是在对象具有复杂的构造逻辑时。
显式调用析构函数:在 Delete 函数中,通过显式调用对象的析构函数,您可以确保对象在释放之前正确地清理其资源。这对于防止资源泄漏和确保对象的正确析构非常重要。
这两个操作一起确保对象池中的对象正确地管理其资源和生命周期。而不是仅仅释放内存,还确保对象的构造和析构逻辑得到执行。
- 注意点3
解决不同系统下数据类型不匹配问题
最后创作不易,点赞支持爱你~