在高性能系统或游戏开发中,频繁的内存分配与释放会导致碎片化、缓存未命中以及 GC 触发。为了解决这些问题,可以自定义一个内存池(Memory Pool),在预先分配一大块内存后按需划分。下面给出一个简单、可扩展的实现示例,并解释关键点与使用方式。
1. 内存池类的设计思路
- 预分配大块:在构造函数里使用
operator new或malloc申请一段连续内存。 - 管理空闲块:可以采用链表或自由列表(Free List)来记录未使用的块。每个块头部保存指向下一个空闲块的指针。
- 对齐:C++ 对象需要特定对齐,内存池也需保证对齐。
- 线程安全:如果多线程使用,可加入互斥锁或使用无锁技术。
下面实现一个最简版本:支持固定大小块的池,块数可在构造时指定。
2. 代码实现
#include <cstddef>
#include <cassert>
#include <mutex>
#include <new> // std::bad_alloc
#include <cstring> // std::memset
class FixedSizePool
{
public:
explicit FixedSizePool(std::size_t blockSize, std::size_t blockCount)
: m_blockSize((blockSize > sizeof(FreeBlock*)) ? blockSize : sizeof(FreeBlock*)),
m_blockCount(blockCount),
m_pool(nullptr),
m_freeList(nullptr)
{
allocatePool();
}
~FixedSizePool()
{
::operator delete(m_pool, std::align_val_t(m_blockSize));
}
void* allocate()
{
std::lock_guard<std::mutex> lock(m_mutex);
if (!m_freeList) {
throw std::bad_alloc(); // pool exhausted
}
// pop one block
FreeBlock* block = m_freeList;
m_freeList = m_freeList->next;
return static_cast<void*>(block);
}
void deallocate(void* ptr)
{
std::lock_guard<std::mutex> lock(m_mutex);
if (!ptr) return; // ignore null
// push back into free list
FreeBlock* block = static_cast<FreeBlock*>(ptr);
block->next = m_freeList;
m_freeList = block;
}
// 禁止拷贝/移动
FixedSizePool(const FixedSizePool&) = delete;
FixedSizePool& operator=(const FixedSizePool&) = delete;
private:
struct FreeBlock
{
FreeBlock* next;
};
void allocatePool()
{
// align allocation to block size
m_pool = ::operator new(m_blockSize * m_blockCount, std::align_val_t(m_blockSize));
// Initialize free list
std::uint8_t* ptr = static_cast<std::uint8_t*>(m_pool);
for (std::size_t i = 0; i < m_blockCount; ++i) {
FreeBlock* block = reinterpret_cast<FreeBlock*>(ptr);
block->next = m_freeList;
m_freeList = block;
ptr += m_blockSize;
}
}
std::size_t m_blockSize;
std::size_t m_blockCount;
void* m_pool;
FreeBlock* m_freeList;
std::mutex m_mutex;
};
关键点说明
-
对齐
::operator new(size_t, std::align_val_t)兼容 C++17 及以后版本,能保证内存块对齐。若使用低版本,可手动使用posix_memalign或aligned_alloc。 -
空闲链表
通过FreeBlock结构把空闲块串联,避免额外的头文件开销。allocate时弹出链表首部,deallocate时将块压回。 -
线程安全
使用std::mutex包裹每次分配/释放操作,简单实现。若性能要求更高,可考虑使用std::atomic及无锁队列。 -
错误处理
当池已满时抛std::bad_alloc,可根据需求自行返回空指针或扩容。
3. 使用示例
int main()
{
const std::size_t blockSize = 64; // 每块 64 字节
const std::size_t blockCount = 1024; // 预分配 1024 块
FixedSizePool pool(blockSize, blockCount);
// 申请 10 块
std::vector<void*> blocks;
for (int i = 0; i < 10; ++i) {
blocks.push_back(pool.allocate());
std::memset(blocks.back(), 0, blockSize); // 可写
}
// 释放
for (void* ptr : blocks) {
pool.deallocate(ptr);
}
return 0;
}
4. 扩展思路
- 可变大小块:将每块前加入一个
size_t字段,记录大小;或者在块前存储元信息(如对象类型)供析构时调用。 - 分层池:针对不同对象大小使用不同的固定池,减少碎片。
- 内存池统计:加入使用计数、峰值等监控,便于调试。
- 自定义分配器:实现
operator new/operator delete,使 STL 容器自动使用池。
5. 小结
自定义内存池能显著降低频繁分配导致的性能瓶颈,尤其在高频场景(如游戏对象、网络协议解析)中表现突出。上述实现以固定大小块为例,易于理解与扩展。结合线程安全与对齐需求,即可在实际项目中快速落地。