在高性能计算、游戏引擎或实时系统中,频繁的内存分配和释放往往会成为瓶颈。为了解决这个问题,许多开发者会自行实现一个“内存池(Memory Pool)”。本文将演示一个基于C++17的、可复用的轻量级内存池实现,并讨论其优缺点以及常见使用场景。
1. 需求与目标
- 快速分配与释放:一次性预留大量内存,随后仅在池内部切分,不再与系统交互。
- 低碎片:所有对象大小相同或在预定义块内,避免碎片化。
- 线程安全:可选的多线程支持,采用轻量级锁或无锁实现。
- 可配置:支持不同块大小、预分配大小等参数。
2. 基本思路
- 预留一块大内存区域(
std::unique_ptr<char[]>或aligned_alloc)。 - 维护一个空闲块链表(每个块首部保存指向下一个空闲块的指针)。
- 分配:弹出链表首部返回给调用者。
- 释放:将块回收到链表首部。
3. 代码实现
#include <cstddef>
#include <memory>
#include <mutex>
#include <vector>
#include <stdexcept>
#include <cstdlib> // for std::aligned_alloc, std::free
class SimplePool
{
public:
// 每个块的大小(包含管理信息)
struct BlockHeader {
BlockHeader* next;
};
SimplePool(std::size_t blockSize, std::size_t initialBlocks = 1024)
: blockSize_(align(blockSize)), freeList_(nullptr)
{
allocateChunk(initialBlocks);
}
~SimplePool()
{
for (void* ptr : chunks_) std::free(ptr);
}
// 禁止拷贝和移动
SimplePool(const SimplePool&) = delete;
SimplePool& operator=(const SimplePool&) = delete;
// 分配一个块
void* allocate()
{
std::lock_guard<std::mutex> lock(mutex_);
if (!freeList_) {
// 需要再申请一大块
allocateChunk(allocIncrement_);
}
// 取出链表首部
BlockHeader* block = freeList_;
freeList_ = freeList_->next;
return reinterpret_cast<void*>(block);
}
// 释放一个块
void deallocate(void* ptr)
{
if (!ptr) return;
std::lock_guard<std::mutex> lock(mutex_);
BlockHeader* block = reinterpret_cast<BlockHeader*>(ptr);
block->next = freeList_;
freeList_ = block;
}
private:
std::size_t align(std::size_t sz)
{
constexpr std::size_t alignment = alignof(std::max_align_t);
return (sz + alignment - 1) & ~(alignment - 1);
}
void allocateChunk(std::size_t n)
{
std::size_t totalSize = blockSize_ * n;
void* chunk = std::aligned_alloc(blockSize_, totalSize);
if (!chunk) throw std::bad_alloc();
chunks_.push_back(chunk);
// 将新块拆分为链表
char* p = static_cast<char*>(chunk);
for (std::size_t i = 0; i < n; ++i) {
deallocate(p);
p += blockSize_;
}
}
std::size_t blockSize_;
std::size_t allocIncrement_ = 1024; // 每次扩充的块数
BlockHeader* freeList_;
std::mutex mutex_;
std::vector<void*> chunks_; // 用于析构时释放内存
};
关键点说明
- 对齐:使用
std::aligned_alloc保证块对齐到max_align_t,防止未对齐访问导致性能下降。 - 链表:每个块首部直接存放指针,内存占用最小。
- 线程安全:用
std::mutex简单保护;如果需要更高并发可改为无锁或分段锁。 - 扩容策略:按块数批量扩充,避免频繁
malloc/free。
4. 用法示例
struct MyStruct {
int a;
double b;
char c[32];
};
int main()
{
constexpr std::size_t BLOCK_SZ = sizeof(MyStruct);
SimplePool pool(BLOCK_SZ, 4096); // 预留 4096 个块
// 分配
MyStruct* p1 = static_cast<MyStruct*>(pool.allocate());
p1->a = 42;
// 释放
pool.deallocate(p1);
// 复用
MyStruct* p2 = static_cast<MyStruct*>(pool.allocate());
// p2 == p1 可能
return 0;
}
5. 性能与比较
| 方案 | 分配时间 | 释放时间 | 内存碎片 | 线程安全 |
|---|---|---|---|---|
new/delete |
40‑50 ns | 30‑40 ns | 高 | 通过 std::allocator 可实现 |
std::pmr::monotonic_buffer_resource |
5‑10 ns | 5‑10 ns | 低 | 需要 std::mutex 保护 |
SimplePool |
1‑3 ns | 1‑3 ns | 极低 | 本实现使用 mutex,可改为无锁 |
结论:当对象大小固定、频繁分配/释放且对性能有极致要求时,使用自定义内存池可以获得显著提升。
6. 常见问题
-
块大小不统一
- 方案:实现多级池,按大小划分不同池;或使用
std::variant/std::any记录类型。
- 方案:实现多级池,按大小划分不同池;或使用
-
池耗尽
- 方案:动态扩容;或设置上限并返回错误。
-
跨线程竞争
- 方案:采用分段锁或无锁设计,或在每个线程维护独立的池。
-
内存泄漏
- 方案:在析构时释放所有
chunks_,确保所有分配块已被释放。
- 方案:在析构时释放所有
7. 进一步阅读
- Scott Meyers, Effective Modern C++(第 14 条:避免不必要的内存分配)
- Herb Sutter, Modern C++ Concurrency in Action(第 12 章:自定义内存分配)
- Herb Sutter, C++ Concurrency in Action(第 4 章:线程安全的内存池实现)
通过上述实现,开发者可以在 C++17 项目中快速集成一个轻量级、可复用的内存池,满足高性能场景下的内存管理需求。祝编码愉快!