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 无法知道该按哪种尺寸回收,且易导致越界写入头部指针。
常见做法是分桶(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 默认删除器接管池内对象。