C++静态内存池
为何需要内存池
在我们为对象分配内存时,我们的编译器会自动在对象对应的内存上下各分配一个cookie,用来描述对象的大小信息,方便我们进行释放。
在我们大量进行内存分配是时候,过多的这些cookies会影响我们的性能。所以,我们希望对于一种对象,我们先分配一大块紧凑的内存(chunk),然后将大量的同种对象存储在这大块内存chunk中。每次需要一个这种对象时,我们不需要再次new一个(这样会创建新的cookie),而是直接从之前申请的那个大块内存中拿就可以。这样,我们就避开了cookie。这个大块的内存,就是我们的“池”。
如何实现内存池
我们申请了chunk之后,需要将其分割成一个个小块来分别存储每一个对象,而每个小块的大小需要可以容纳该对象。在实现时,我们通常根据具体应用场景以及经验,手动指定小块的大小。
每一个空闲的,未分配的小块之间使用单链表进行连接。每个小块就是单链表的一个节点node。当将这个小块被使用到时,就将它移除链表。回收时,将它重新加入链表。
#ifndef _StaticAllocator
#define _StaticAllocator
#include <iostream>
#include <cstddef> // for std::size_t
#include <new> // for std::bad_alloc
class StaticAllocator {
public:
static constexpr std::size_t POOL_SIZE = 1024; // 内存池的总大小
static constexpr std::size_t BLOCK_SIZE = 32; // 每个内存块的大小
// 获取单例实例
static StaticAllocator& getInstance() {
static StaticAllocator instance;
return instance;
}
// 禁用拷贝构造和赋值运算符
StaticAllocator(const StaticAllocator&) = delete;
StaticAllocator& operator=(const StaticAllocator&) = delete;
// 分配内存块
void* allocate() {
if (!freeList) {
throw std::bad_alloc();
}
void* block = freeList;
freeList = freeList->next;
return block;
}
// 释放内存块
void deallocate(void* block) {
Node* node = static_cast<Node*>(block);
node->next = freeList;
freeList = node;
}
StaticAllocator() {
pool = ::operator new(POOL_SIZE);
freeList = nullptr;
// 初始化内存池,并将所有块加入空闲列表
for (std::size_t i = 0; i < POOL_SIZE; i += BLOCK_SIZE) {
deallocate(static_cast<char*>(pool) + i);
}
}
~StaticAllocator() {
::operator delete(pool);
}
private:
struct Node {
Node* next;
};
void* pool;
Node* freeList;
};
// 重载operator new和operator delete运算符,使用静态内存池
#define DECLARE_POOL_ALLOC()\
public:\
static StaticAllocator allocator; \
static void* operator new(std::size_t size) {\
if (size > StaticAllocator::BLOCK_SIZE) {\
throw std::bad_alloc();\
}\
return allocator.allocate();\
}\
static void operator delete(void* ptr, std::size_t size) noexcept {\
allocator.deallocate(ptr); \
}\
#define IMPLEMENT_POOL_ALLOC(classname) StaticAllocator classname::allocator;
#endif // !_StaticAllocator
问题:使用链表需要额外存储next指针,在这样是否造成额外开销,效果适得其反?
不会。内存池创建时,我们先分配chunk大小的内存块,然后将该内存块类型转换为node,并在其中存储next指针。在将该小内存块分配出去时,我们直接将该小内存块类型转换为对应的对象的类型,并存储其数据,next指针便被覆盖掉了。再次回收时,再将该内存块类型转换为node,并在其中存储next指针。可以认为next指针和对象之间是分时共用空间的关系。
下面是一个测试类
#ifndef _TESTCLASS
#define _TESTCLASS
#include "StaticAllocator.h"
// 示例类,使用静态内存池进行内存分配
class MyClass {
DECLARE_POOL_ALLOC()
public:
MyClass() {
std::cout << "MyClass Constructor\n";
}
~MyClass() {
std::cout << "MyClass Destructor\n";
}
private:
int data[7]; // 使对象大小接近32字节
};
IMPLEMENT_POOL_ALLOC(MyClass)
#endif
main.cpp
#include"TestClass.h"
#include<iostream>
int main() {
try {
MyClass* obj1 = new MyClass();
MyClass* obj2 = new MyClass();
MyClass* obj3 = new MyClass();
std::cout << obj1 << std::endl;
std::cout << obj2 << std::endl;
std::cout << obj3 << std::endl;
delete obj1;
delete obj2;
delete obj3;
}
catch (const std::bad_alloc& e) {
std::cerr << "Allocation failed: " << e.what() << std::endl;
}
return 0;
}
运行结果(地址是16位表示的)
每两个对象之间都相差32的距离,是紧凑分配的。