new/delete在高频小对象场景变慢,因频繁系统调用、堆管理器锁竞争与内存碎片;内存池通过预分配大块内存+原子空闲链表实现无锁高效分配。

为什么 new / delete 在高频小对象场景下会变慢
频繁调用 new 和 delete 本质是向操作系统申请/释放页内存(mmap/brk),再经由 libc 的堆管理器(如 ptmalloc)切分、合并、加锁。小对象(比如几十字节的节点)反复分配时,会产生大量元数据开销、锁竞争和内存碎片。实测中,一个每秒百万次的 new Node 可能比池化慢 3–10 倍,且 GC 式压力会让 malloc 内部链表遍历变长。
堆内存池的核心思路是:一次性向系统申请一大块内存(如 64KB),自己维护空闲块链表,alloc 直接取头节点,free 仅把指针插回链表——全程无系统调用、无锁(单线程)或轻量 CAS(多线程)。
如何手写一个线程安全的固定大小块内存池
以 32 字节对象为例,不依赖模板、不封装类,聚焦核心逻辑。关键点在于:块对齐、头部元信息、原子空闲链表操作。
-
malloc一次申请足够多的连续内存(如size_t pool_size = 64 * 1024),用aligned_alloc(alignof(std::max_align_t), pool_size)确保地址对齐 - 每个块头部存一个
char*指针(8 字节),指向下一个空闲块;实际可用内存从该指针后偏移开始(即block + sizeof(char*)) - 初始化时,把整块内存切成等长块,串成单向链表:
next_ptr = (char**)block; *next_ptr = next_block; - 分配时用
std::atomic_load读取链表头,std::atomic_compare_exchange_weak原子摘下;释放时同样原子插入头部
// 简化版核心分配逻辑(无错误检查) static std::atomicfree_list{nullptr}; void init_pool() { char pool = static_cast
>(aligned_alloc(alignof(std::max_align_t), 65536)); const size_t block_size = 32; char p = pool; for (size_t i = 0; i < 65536 / block_size - 1; ++i) { char next = reinterpret_cast >(p); next = p + block_size; p += block_size; } char last = reinterpret_cast>(p); *last = nullptr; free_list.store(pool, std::memory_order_relaxed); } void pool_alloc() { char head = free_list.load(std::memory_order_acquire); char next; while (head && !free_list.compare_exchange_weak(head, next = (char**)head, std::memory_order_acq_rel, std::memory_order_acquire)) {} return head; }
void pool_free(void ptr) { if (!ptr) return; char next_ptr = reinterpret_cast
>(ptr); char old_head = free_list.load(std::memory_order_acquire); do { next_ptr = old_head; } while (!free_list.compare_exchange_weak(old_head, static_cast>(ptr), std::memory_order_acq_rel, std::memory_order_acquire)); }
如何支持多种对象大小并避免跨块访问
单一大小池只适用于特定场景(如链表节点、事件结构体)。若需多尺寸,不能简单复用同一块内存——否则 free 无法知道该按哪种尺寸回收,且易导致越界写入头部指针。
立即学习“C++免费学习笔记(深入)”;
常见做法是分桶(bucket):按 2 的幂次划分尺寸档位(如 16B、32B、64B、128B…),每个档位维护独立的 free_list 和预分配池。分配时向上取整到最近档位,free 必须传入原始分配尺寸(或由池记录),否则无法定位所属桶。
- 用
constexpr计算档位索引:int bucket = std::bit_width(size_t(size)) - 4(假设最小 16B) - 每个桶的池可延迟创建:首次请求某尺寸时才
malloc一块,避免冷启动浪费 - 注意:若对象含虚函数或需要构造/析构,
pool_alloc返回的内存未调用构造函数,必须显式new (ptr) T{...};同理pool_free前要手动调用obj.~T()
自定义 free 逻辑时最容易忽略的三个细节
很多人以为“重载 operator delete 就完事”,但实际落地时这几个点常导致崩溃或泄漏:
-
operator delete接收的是void*,但你无法从中还原对象类型或尺寸——除非在分配时额外存储元数据(如前缀加 4 字节 size 字段),否则free无法知道该归还给哪个桶 - 全局重载
operator new/operator delete会影响所有代码,包括 STL 容器内部(std::vector的扩容)、第三方库;更安全的做法是仅对特定类重载成员版本:class Node { void* operator new(size_t); void operator delete(void*) noexcept; }; - 多线程下,如果多个线程同时
free同一池,而你的链表插入没用原子操作或互斥锁,会破坏next指针,造成后续alloc返回非法地址——这种 bug 往往偶发且难以复现
真正稳定的池管理,不是“替换 new/delete”,而是明确控制生命周期:对象在哪创建、谁负责销毁、是否允许跨线程传递。一旦引入自定义 free,就必须同步约束使用边界,比如禁止 std::shared_ptr 默认删除器接管池内对象。











