new/delete在高频小对象场景下拖慢程序,因其每次调用均需系统堆管理、锁竞争及内存对齐等开销;内存池通过预分配+自主管理空闲链表绕过系统堆,但仅适用于固定大小、生命周期可控的小对象。

为什么 new 和 delete 在高频小对象场景下会拖慢程序
因为每次调用 new 都要进入系统堆管理器,做空闲链表查找、内存对齐、锁竞争(多线程下)、甚至触发 brk/mmap 系统调用。小对象(比如几十字节的节点)反复分配释放时,这些开销远超实际内存使用成本。
内存池本质是「预分配一大块内存 + 自己管理内部碎片」,绕过系统堆。但别一上来就写通用池——先从最简单的固定大小对象池开始。
- 适用场景:
std::list<Node>、std::vector<std::shared_ptr<T>>这类频繁 new/delete 同构小对象的结构 - 关键约束:所有对象大小必须一致,且生命周期大致可控(避免长期持有导致池内碎片)
- 不解决的问题:
malloc大小不一、需要realloc、跨线程共享池(需额外加锁或 TLS)
手写一个线程安全的固定大小内存池(C++17)
核心思路:用 std::vector<char> 预分配内存块,用单向自由链表管理空闲槽位,std::atomic<void*> 做无锁头指针(CAS 实现)。
注意:不是所有“无锁”都真快,小规模竞争下 std::mutex 可能更稳;这里选 CAS 是为了演示最小依赖,实际项目可按需替换。
立即学习“C++免费学习笔记(深入)”;
- 构造时传入单个对象大小
obj_size和总槽数capacity,内部按对齐向上取整(用alignof(std::max_align_t)或alignas) - 分配时只改原子指针,不查链表长度——所以必须确保初始化时把所有槽位串成链表,否则
allocate()会返回nullptr - 析构前必须保证所有对象已
deallocate(),否则内存泄漏且池内链表错乱
class FixedPool {
std::vector<char> memory_;
std::atomic<void*> free_list_{nullptr};
size_t obj_size_;
public:
FixedPool(size_t obj_size, size_t capacity) : obj_size_(obj_size), memory_(obj_size * capacity) {
char* ptr = memory_.data();
for (size_t i = 0; i < capacity - 1; ++i) {
*reinterpret_cast<void**>(ptr) = ptr + obj_size;
ptr += obj_size;
}
*reinterpret_cast<void**>(ptr) = nullptr; // tail
free_list_.store(memory_.data());
}
<pre class='brush:php;toolbar:false;'>void* allocate() {
void* old = free_list_.load();
void* desired;
do {
if (!old) return nullptr;
desired = *reinterpret_cast<void**>(old);
} while (!free_list_.compare_exchange_weak(old, desired));
return old;
}
void deallocate(void* p) {
void* old = free_list_.load();
do {
*reinterpret_cast<void**>(p) = old;
} while (!free_list_.compare_exchange_weak(old, p));
}};
operator new 和 operator delete 全局重载的坑
很多人想“一劳永逸”地替掉所有 new,结果发现 std::string、std::vector 内部还在偷偷调用全局 malloc,甚至 STL 容器的临时缓冲区也不走你的池。
真正可控的只有你显式控制的对象类型——比如给某个类单独重载成员版 operator new:
- 成员重载优先级高于全局,且只影响该类的
new调用,不影响其成员变量的分配(除非成员也重载了) - 必须同时提供
operator delete,且参数签名严格匹配(包括noexcept),否则析构时可能调用默认delete导致崩溃 - 如果类有虚函数,
operator new分配的内存还要留出虚表指针空间,务必确保池内块足够大
示例:
struct Node {
int data;
Node* next;
static FixedPool pool;
void* operator new(size_t) { return pool.allocate(); }
void operator delete(void* p) noexcept { pool.deallocate(p); }
};
FixedPool Node::pool(sizeof(Node), 1024);
什么时候该放弃手写内存池
当你开始为对齐、多尺寸、回收合并、线程局部缓存、与 std::allocator 适配而加代码时,说明已经触达复杂度拐点。
生产环境更推荐直接集成成熟方案:
-
tcmalloc(Google):LD_PRELOAD 即可生效,自动优化小对象分配,附带性能分析工具 -
jemalloc(FreeBSD):对多核扩展性更好,MALLOC_CONF="lg_chunk:21"可调参 - C++20 的
std::pmr::monotonic_buffer_resource:适合短生命周期批量对象(如解析一次 JSON 的所有节点),但不可回收单个对象
自己写的池最难调试的永远不是分配逻辑,而是对象析构顺序和池生命周期之间的耦合——比如静态对象析构时还在用池,或者池被销毁后仍有对象指向其内存。










