https://blog.csdn.net/chenlong_cxy/article/details/122819562# 如何实现定长
非模板类型
当使用非类型模板参数定义内存池时,内存池中申请的对象大小会固定为 N。这种设计确保了内存池中每个对象的大小都一致,从而简化了内存管理。
固定大小:
使用非类型模板参数时,可以在模板中指定一个大小 N,这意味着所有通过该内存池分配的对象都将具有相同的大小 N。
类型无关:
这种方法与具体的对象类型无关,它仅关注对象的大小。因此,您可以创建一个内存池,专门用于分配固定大小的内存块,而不考虑对象的实际类型。
template<size_t N> 是 C++ 中使用非类型模板参数的一个示例。在这里,N 是一个编译时常量,用于表示模板的大小。具体来说,这种模板设计允许我们创建定长内存池或其他数据结构,其对象的大小在编译时就确定
#include <iostream>
#include <vector>
#include <cstdlib>
template<size_t N> // N 是对象大小
class ObjectPool {
public:
ObjectPool(size_t poolSize) {
pool.reserve(poolSize);
for (size_t i = 0; i < poolSize; ++i) {
void* obj = ::operator new(N); // 分配 N 字节的内存
pool.push_back(obj); // 将指针存入池中
}
}
~ObjectPool() {
for (auto obj : pool) {
::operator delete(obj); // 释放内存
}
}
void* allocate() {
if (pool.empty()) return nullptr; // 如果池为空,返回空指针
void* obj = pool.back(); // 获取池中的最后一个对象
pool.pop_back(); // 从池中移除该对象
return obj; // 返回对象指针
}
void deallocate(void* obj) {
pool.push_back(obj); // 将对象放回池中
}
private:
std::vector<void*> pool; // 存储可用对象的内存池
};
// 使用示例
int main() {
const size_t objectSize = 16; // 每个对象占用 16 字节
ObjectPool<objectSize> pool(10); // 创建一个对象池,最多存储 10 个对象
void* obj1 = pool.allocate(); // 从内存池中分配一个对象
std::cout << "Allocated object at: " << obj1 << " with size: " << objectSize << " bytes." << std::endl;
pool.deallocate(obj1); // 将对象释放回内存池
return 0;
}
ObjectPool pool(10); 这行代码确实是创建了一个内存池的实例,负责管理最多可以存储 10 个大小为 objectSize 字节的对象。这种设计模式特别适合高性能场景下的内存管理。
这里的内存池为10个大小的objectSize 的对象,当内存池空间使用完了的话,可以抛出异常返回nullptr;也可以扩展内存池,内存池可以再分配一块新的内存区域,并将新内存区域的对象添加到池中;
模板参数实现定长内存池
内存池中的对象大小固定。使用模板类可以让我们在创建对象池时指定对象的类型,内存池将为该类型的对象提供内存管理。下面是一个简单的对象池实现示例,展示了如何通过模板来创建一个定长内存池。
#include <iostream>
#include <vector>
#include <stdexcept>
template <class T>
class ObjectPool {
public:
ObjectPool(size_t size) : poolSize(size), freeList(size) {
// 预分配内存并初始化空闲链表
for (size_t i = 0; i < poolSize; ++i) {
freeList[i] = new T(); // 分配对象
available.push_back(freeList[i]); // 将对象加入可用列表
}
}
~ObjectPool() {
// 清理内存
for (T* obj : freeList) {
delete obj;
}
}
T* allocate() {
if (available.empty()) {
throw std::runtime_error("No available objects in the pool");
}
// 从可用列表中取出一个对象
T* obj = available.back();
available.pop_back();
return obj;
}
void deallocate(T* obj) {
// 将对象放回可用列表
available.push_back(obj);
}
private:
size_t poolSize;
std::vector<T*> freeList; // 存储预分配的对象
std::vector<T*> available; // 存储可用的对象
};
// 示例用法
int main() {
try {
ObjectPool<int> intPool(5); // 创建一个对象池,最多存储 5 个 int 对象
// 分配对象
int* a = intPool.allocate();
int* b = intPool.allocate();
// 使用对象
*a = 10;
*b = 20;
std::cout << "Allocated values: " << *a << ", " << *b << std::endl;
// 释放对象
intPool.deallocate(a);
intPool.deallocate(b);
// 再次分配对象
int* c = intPool.allocate();
std::cout << "Allocated value: " << *c << std::endl;
// 清理
intPool.deallocate(c);
} catch (const std::runtime_error& e) {
std::cerr << "Error: " << e.what() << std::endl;
}
return 0;
}
使用模板参数的定长内存池(如 ObjectPool)的确只能管理一种特定类型的对象。每次创建一个对象池时,模板参数 T 决定了该内存池能够分配的对象类型。例如,如果你创建一个 ObjectPool,这个对象池就只能管理 int 类型的对象,而无法管理其他类型的对象。
可以通过继承多态实现多个对象类型的管理
如何直接向堆申请空间
malloc 确实是标准 C++ 库中用于动态内存分配的函数,通常它会在后台调用操作系统提供的底层内存分配函数(如 brk、sbrk 或 mmap),以便为程序分配内存。而在实现定制的内存池时,直接使用 mmap 或其他低级内存分配方法,可以获得更多控制权和灵活性。
malloc 和 mmap 的区别
分配方式:
malloc 通常使用堆(heap)来管理内存,堆的分配和释放通常需要维护一些元数据,这可能导致内存碎片。
mmap 直接向操作系统请求一块虚拟内存区域,可以更高效地处理大块内存请求,并且提供了灵活的内存管理策略。
用途:
malloc 适用于一般用途的动态内存分配。
mmap 更常用于需要大块内存、共享内存或需要精细控制内存管理的场景
#include <iostream>
#include <stdexcept>
#ifdef _WIN32
#include <Windows.h>
#else
#include <sys/mman.h>
#include <unistd.h>
#endif
// 直接去堆上申请按页申请空间
inline static void* SystemAlloc(size_t kpage) {
#ifdef _WIN32
void* ptr = VirtualAlloc(0, kpage << 13, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
#else
void* ptr = mmap(nullptr, kpage * 4096, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (ptr == MAP_FAILED) {
ptr = nullptr; // mmap失败处理
}
#endif
if (ptr == nullptr) {
throw std::bad_alloc();
}
return ptr;
}
template <typename T>
class ObjectPool {
public:
ObjectPool(size_t size) : poolSize(size) {
// 向系统申请内存
memory = SystemAlloc(size);
// 进一步的初始化逻辑,例如构造对象
}
~ObjectPool() {
// 释放内存
free(memory);
}
private:
size_t poolSize;
void* memory; // 存储申请的内存地址
};
// 示例用法
int main() {
try {
ObjectPool<int> intPool(10); // 创建一个存储10个int的对象池
} catch (const std::bad_alloc& e) {
std::cerr << "Memory allocation failed: " << e.what() << std::endl;
}
return 0;
}
通过封装内存分配,您可以创建一个便于管理的对象池。无论是在 Windows 还是 Linux 平台,使用 SystemAlloc 函数都能简单地申请内存。此外,使用 try-catch 语句处理可能的内存分配异常,确保程序的健壮性。这样的设计模式不仅增强了代码的可移植性,还提高了内存管理的灵活性。
定长内存池实现代码
//定长内存池
template<class T>
class ObjectPool
{
public:
//申请对象
T* New()
{
T* obj = nullptr;
//优先把还回来的内存块对象,再次重复利用
if (_freeList != nullptr)
{
//从自由链表头删一个对象
obj = (T*)_freeList;
_freeList = NextObj(_freeList);
}
else
{
//保证对象能够存储得下地址
size_t objSize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);
//剩余内存不够一个对象大小时,则重新开大块空间
if (_remainBytes < objSize)
{
_remainBytes = 128 * 1024;
//_memory = (char*)malloc(_remainBytes);
_memory = (char*)SystemAlloc(_remainBytes >> 13);
if (_memory == nullptr)
{
throw std::bad_alloc();
}
}
//从大块内存中切出objSize字节的内存
obj = (T*)_memory;
_memory += objSize;
_remainBytes -= objSize;
}
//定位new,显示调用T的构造函数初始化
new(obj)T;
return obj;
}
//释放对象
void Delete(T* obj)
{
//显示调用T的析构函数清理对象
obj->~T();
//将释放的对象头插到自由链表
NextObj(obj) = _freeList;
_freeList = obj;
}
private:
char* _memory = nullptr; //指向大块内存的指针
size_t _remainBytes = 0; //大块内存在切分过程中剩余字节数
void* _freeList = nullptr; //还回来过程中链接的自由链表的头指针
};
性能对比
下面我们将实现的定长内存池和malloc/free进行性能对比,测试代码如下:
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 = 1000000;
std::vector<TreeNode*> v1;
v1.reserve(N);
//malloc和free
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();
//定长内存池
ObjectPool<TreeNode> TNPool;
std::vector<TreeNode*> v2;
v2.reserve(N);
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;
}
高并发内存池
现代很多的开发环境都是多核多线程,因此在申请内存的时,必然存在激烈的锁竞争问题。malloc本身其实已经很优秀了,但是在并发场景下可能会因为频繁的加锁和解锁导致效率有所降低,而该项目的原型tcmalloc实现的就是一种在多线程高并发场景下更胜一筹的内存池。
在实现内存池时我们一般需要考虑到效率问题和内存碎片的问题,但对于高并发内存池来说,我们还需要考虑在多线程环境下的锁竞争问题。