内存池通过预分配大块内存并用链表管理空闲块,避免malloc/new的频繁系统调用、锁竞争和元数据开销;FixedBlockPool以固定大小块+单链表实现O(1)分配/释放,需注意对象大小一致、生命周期可控及分配器成对使用。

为什么 malloc 和 new 在高频小对象场景下会拖慢性能
频繁调用 malloc 或 new 分配小块内存(比如几十字节的节点),会导致堆管理器反复拆分/合并空闲块、维护元数据、加锁(多线程下),甚至触发系统调用。实际测下来,分配一个 32 字节对象可能比构造对象本身还慢 5–10 倍。
内存池的核心思路是:一次性向系统申请一大块内存(比如 64KB),内部用链表或位图管理空闲块,allocate() 只做指针偏移 + 链表摘除,deallocate() 只做链表插入 —— 全部在用户态完成,无锁(单线程)或轻量锁(多线程)。
- 适用场景:
std::list节点、事件循环中的task结构、游戏里大量particle对象 - 不适用场景:对象大小差异大(如同时有 16B 和 2KB 对象)、生命周期不规则(无法批量回收)
- 关键约束:必须成对使用自定义分配器,不能混用
new/delete和池内指针
如何写一个线程安全的固定大小内存池(FixedBlockPool)
固定块池最简单也最常用。假设你要为 struct Node { int x; Node* next; }; 提供分配器,大小固定为 16 字节(含对齐)。
核心结构体只需三个成员:m_block_size(块大小)、m_free_list(指向空闲块的 char* 单链表)、m_blocks(原始大内存块首地址)。所有操作围绕链表头进行:
立即学习“C++免费学习笔记(深入)”;
class FixedBlockPool {
size_t m_block_size;
char* m_blocks;
char* m_free_list;
public:
FixedBlockPool(size_t block_sz, size_t n_blocks)
: m_block_size{block_sz}, m_free_list{nullptr} {
const size_t total = n_blocks * block_sz;
m_blocks = static_cast(::operator new(total));
// 构建初始空闲链表:每个块头存下一个空闲块地址
for (size_t i = 0; i < n_blocks; ++i) {
char* p = m_blocks + i * block_sz;
*reinterpret_cast(p) = m_free_list;
m_free_list = p;
}
}
void* allocate() {
if (!m_free_list) return nullptr;
char* p = m_free_list;
m_free_list = *reinterpret_cast(p); // 摘链
return p;
}
void deallocate(void* p) {
if (!p) return;
*reinterpret_cast(p) = m_free_list; // 入链
m_free_list = static_cast(p);
} };
- 注意:这里没加锁,多线程需用
std::atomic 替代裸指针,allocate() 改用 compare_exchange_weak
-
::operator new 绕过 new_handler,避免递归;别用 malloc,它不保证对齐
- 释放时不做校验 —— 若传入非法地址,行为未定义;生产环境可加
assert(p >= m_blocks && p
怎么让 std::vector 或 std::unordered_map 用上你的内存池
C++ 容器支持模板参数 Allocator,但不是直接传池对象,而是传满足 Allocator 要求的类型。你需要封装一个符合标准的分配器类,例如 PoolAllocator:
template
class PoolAllocator {
static inline FixedBlockPool* s_pool = nullptr;
public:
using value_type = T;
PoolAllocator() = default;
template constexpr PoolAllocator(const PoolAllocator&) noexcept {}
T* allocate(size_t n) {
if (n != 1) throw std::bad_alloc(); // 固定块池只支持单对象
if (!s_pool) s_pool = new FixedBlockPool(alignof(T), sizeof(T), 1024);
return static_cast(s_pool->allocate());
}
void deallocate(T* p, size_t n) {
if (n == 1) s_pool->deallocate(p);
} };
然后这样使用:
std::vector> vec;
std::unordered_map, std::equal_to<>,
PoolAllocator>> map;
- 注意:不同
T 类型必须对应不同池实例(int 和 Node 不能共用同一 FixedBlockPool)
-
std::allocator_traits 会自动处理 construct/destroy,你不用重写它们
- 容器析构时不会自动销毁池 —— 记得在程序退出前手动
delete s_pool,否则泄漏
容易被忽略的对齐和异常安全细节
内存池返回的指针必须满足 T 的对齐要求(alignof(T)),否则 new 表达式或 std::construct_at 可能崩溃。固定块池里,块大小必须是 alignof(T) 的整数倍,且首地址本身也要对齐。
- 申请大内存时用
aligned_alloc(C++17)或 _aligned_malloc(Windows),而不是 malloc
- 若
allocate() 失败(m_free_list == nullptr),应抛出 std::bad_alloc,而非返回 nullptr —— 标准容器不检查返回值,会直接解引用空指针
- 不要在
deallocate() 里调用 delete 或 free;也不要试图在池中 delete 用 new 分配的对象 —— 这是未定义行为
- 调试时可给每块加 magic number(如 0xDEADBEEF),在
allocate/deallocate 里校验,快速定位 double-free 或越界写
真正难的不是写出来,而是确保所有路径都走池、所有析构都正确触发、所有线程竞争都被覆盖到。先从单线程 + 一种类型跑通,再加锁,再扩展多类型支持。










