作为程序员(C/C++)我们知道申请内存使用的是malloc,malloc其实就是一个通用的大众货(无法专门为某个场景做优化),什么场景下都可以用,但是什么场景下都可以用就意味着什么场景下都不会有很高的性能,下面我们就先来设计一个定长内存池做个开胃菜,当然这个定长内存池在我们后面的高并发内存池(专门针对高并发场景)中也是有价值的,所以学习他目的有两层,先熟悉一下简单内存池是如何控制的,第二他会作为我们后面内存池的一个基础组件。
自由链表(Free List)
自由链表(Free List)是一种常见的数据结构,通常用于内存管理或对象管理。 在内存管理中,自由链表用于跟踪已分配但当前未使用的内存块。这些内存块通过指针链接在一起形成一个链表,当需要分配新的内存时,可以从自由链表中取出一个合适的内存块进行使用;当释放内存时,将其添加回自由链表。 在对象管理中,自由链表可以用于管理一组可重复使用的对象。当需要创建新对象时,如果自由链表非空,可以直接从链表中取出一个对象进行初始化和使用;当对象不再需要时,将其放回自由链表,而不是直接删除,以提高对象创建和销毁的效率。 自由链表的优点包括减少内存分配和释放的开销、提高性能等,但也需要注意管理链表的正确性和避免出现内存泄漏等问题。
- 先用一个指针_memory指向一大块空间作内存池(malloc直接开辟的),
那这个指针用什么类型的呢?void*的,无具体类型,无法移动指针;char*能更精准的控制指针移动的距离;
- 然后通过模板类型参数T的大小来得知,使用者要获取一个多大的空间的对象。
逻辑上一块小空间被切走分用,但物理上没变还是连续的一大块空间。
- 归还被分用的空间:归还/分用的顺序可能不一样,我们无法得知这一次归还的是内存池中的哪一块空间。
- 解决:用自由链表组织管理,用归还空间的前4/8(32/64位机器)字节来存储下一块归还空间的地址,每一块归还空间就是一个节点。
- 如何做?访问前4/8字节空间+链入_freeList
- 缺点:若 归还空间 < 4/8字节,会不够存储地址。解决:
//在一开始切割分用大块空间时,就保证切割的空间块至少要能存储地址的大小
obj = (T*)_memory;
size_t objSize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);
_memory += objSize;
_remainBytes -= objSize;
- 内存池剩余空间为0时,_memory会指向未知/没有权限的地址。
- 如果此时还需要分用,是用realloc扩/缩容,还是 malloc 再开辟一大块空间?
- 如果用realloc可能会重新全部分配空间地址,那自由链表中的地址就全部作废了。用malloc可以避免。
- 内存池剩余空间 < T的大小(一个对象),那也只能浪费丢掉了,用malloc再开辟一大块空间(之前的首地址已经使用了,不用记录)。
- 向内存池申请空间时,优先使用_freeList上的。因为如果先使用_memory上的,用完后会直接再开辟,没必要(可能_freeList还有空间呢)。
- 定位new表达式,让obj指向一个完整的对象(对指向的空间初始化)
// 定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。
// 使用格式:
// new (place_address) type或者new (place_address) type(initializer-list)
// place_address必须是一个指针,initializer-list是类型的初始化列表
// 使用场景:
// 定位new表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,
// 所以如果是自定义类型的对象,需要使用new的定义表达式进行显示调构造函数进行初始化。
//obj现在指向的只不过是与A对象相同大小的一段空间,还不能算是一个对象,因为构造函数没有执行
new (obj) T; // 定位new,显示调用T的构造函数初始化,让obj指向真正的对象。把obj指向的空间格式成T类型的(错)
- 内存池不用释放(可以一直被当前进程使用,进程结束直接释放对应物理空间),
- 也没法释放(不知道哪些对象使用的是内存池,不知道有没有都归还,malloc开辟内存池时的地址没有特意都记录,无法free释放对应的空间,free释放的必须是malloc...返回的地址,不能是随便一个地址)。
#include <iostream>
#include <vector>
#include <time.h>
using std::cout;
using std::endl;
#ifdef _WIN32
#include <windows.h>
#else
//
#endif
// 定长内存池
// template<size_t N> //每次使用的大小:模板用 常量/类型 都一样,因为都是固定的
// class ObjectPool
//{};
// 直接去堆上按页申请空间
inline static void *SystemAlloc(size_t kpage)
{
#ifdef _WIN32
void *ptr = VirtualAlloc(0, kpage << 13, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);//<<13位 = /8KB
#else
// linux下brk mmap等
#endif
if (ptr == nullptr)
throw std::bad_alloc();
return ptr;
}
template <class T>
class ObjectPool
{
public:
T *New()
{
T *obj = nullptr;
// 优先把还回来内存块对象,再次重复利用
if (_freeList)
{
void *next = *((void **)_freeList);
obj = (T *)_freeList;
_freeList = next;
}
else
{
// 剩余内存不够一个对象大小时,则重新开大块空间
if (_remainBytes < sizeof(T))
{
_remainBytes = 128 * 1024;//128KB
//_memory = (char*)malloc(_remainBytes);
_memory = (char *)SystemAlloc(_remainBytes >> 13);//2^13=8KB//绕过malloc,直接使用Windows的系统调用在堆上开空间
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;
}
// 定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。
// 使用格式:
// new (place_address) type或者new (place_address) type(initializer-list)
// place_address必须是一个指针,initializer-list是类型的初始化列表
// 使用场景:
// 定位new表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new的定义表达式进行显示调构造函数进行初始化。
// p1现在指向的只不过是与A对象相同大小的一段空间,还不能算是一个对象,因为构造函数没有执行
new (obj) T; // 定位new,显示调用T的构造函数初始化,让obj指向真正的对象。把obj指向的空间格式成T类型的(错)
return obj;
}
void Delete(T *obj)
{
// 显示调用析构函数清理对象
obj->~T();
// 头插
*(void **)obj = _freeList;
_freeList = obj;
}
private:
char *_memory = nullptr; // 指向大块内存的指针;用char* 能更精准的控制指针移动的距离
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 = 5;
// 每轮申请释放多少次
const size_t N = 100000;
std::vector<TreeNode *> v1;
v1.reserve(N);
size_t begin1 = clock();
for (size_t j = 0; j < Rounds; ++j)
{
for (int i = 0; i < N; ++i)
{
v1.push_back(new TreeNode);
}
for (int i = 0; i < N; ++i)
{
delete v1[i];
}
v1.clear();
}
size_t end1 = clock();
std::vector<TreeNode *> v2;
v2.reserve(N);
ObjectPool<TreeNode> TNPool;
size_t begin2 = clock();
for (size_t j = 0; j < Rounds; ++j)
{
for (int i = 0; i < N; ++i)
{
v2.push_back(TNPool.New());
}
for (int i = 0; i < N; ++i)
{
TNPool.Delete(v2[i]);
}
v2.clear();
}
size_t end2 = clock();
cout << "new cost time:" << end1 - begin1 << endl;
cout << "object pool cost time:" << end2 - begin2 << endl;
}