背景
写 C++ 服务或者引擎代码时,经常会碰到一种情况:CPU 看着不高,但延迟就是压不下去。
最后一 profile,热点不在算法,也不在锁,而是在 operator new 和 operator delete。
这类问题在下面几种场景里尤其常见:
- 网络服务里大量创建短生命周期请求对象
- 游戏引擎里频繁分配小型组件
- 消息队列消费者持续构造临时 buffer
如果对象大小固定,或者分布相对集中,内存池通常是很直接的一刀。
为什么默认分配器会成为瓶颈
通用分配器要解决的问题很多:
- 不同尺寸的内存申请
- 跨线程竞争
- 碎片整理
- 对齐要求
这些能力都很重要,但它们也意味着额外开销。
如果你的场景很单一,比如“每次都申请一个 256 字节的请求对象”,那继续走通用分配器其实是在为用不到的能力买单。
一个简单的固定块内存池
先看一个足够说明问题的版本。
#include <cstddef>
#include <new>
#include <vector>
class MemoryPool {
public:
MemoryPool(std::size_t blockSize, std::size_t blockCount)
: block_size_(blockSize) {
data_.resize(blockSize * blockCount);
for (std::size_t i = 0; i < blockCount; ++i) {
void* ptr = data_.data() + i * blockSize;
free_list_.push_back(ptr);
}
}
void* allocate() {
if (free_list_.empty()) {
throw std::bad_alloc();
}
void* ptr = free_list_.back();
free_list_.pop_back();
return ptr;
}
void deallocate(void* ptr) {
free_list_.push_back(ptr);
}
private:
std::size_t block_size_;
std::vector<char> data_;
std::vector<void*> free_list_;
};
思路很直接:
- 启动时一次性申请一大块内存
- 按固定大小切成很多块
- 用空闲链表管理可用块
优点也很明显:
- 分配是 O(1)
- 回收是 O(1)
- 没有额外碎片问题
和对象生命周期配合
内存池只负责“给你一块内存”,对象构造和析构还是要自己管。
template <typename T>
class ObjectPool {
public:
explicit ObjectPool(std::size_t count)
: pool_(sizeof(T), count) {}
template <typename... Args>
T* create(Args&&... args) {
void* memory = pool_.allocate();
return new (memory) T(std::forward<Args>(args)...);
}
void destroy(T* object) {
if (object == nullptr) {
return;
}
object->~T();
pool_.deallocate(object);
}
private:
MemoryPool pool_;
};
这里用到了 placement new。它只做构造,不分配内存。
struct RequestContext {
int fd;
std::string path;
std::string trace_id;
};
int main() {
ObjectPool<RequestContext> pool(1024);
auto* ctx = pool.create();
ctx->fd = 10;
ctx->path = "/api/orders";
ctx->trace_id = "abc123";
pool.destroy(ctx);
}
什么时候值得上内存池
不是所有代码都该自己写分配器。
我一般只在这几种场景考虑:
- profile 证明确实有分配热点
- 对象大小相对固定
- 对象生命周期短且数量大
- 对尾延迟敏感
如果只是偶尔创建几个对象,直接 new/delete 更简单,也更不容易出错。
线程安全问题
上面的实现是单线程版本。到了多线程环境,就得认真处理并发访问。
常见方案:
- 每线程一个本地池,减少锁竞争
- 中央池 + 线程本地缓存
- 无锁 free list,但实现复杂度明显上升
在高并发服务里,我更倾向于线程局部池 + 批量回收。因为真正影响尾延迟的,很多时候不是分配本身,而是锁竞争和 cache miss。
常见坑点
1. 池化了不该池化的对象
像 std::string、std::vector 这种内部自己还会分配堆内存的对象,池化外层对象不一定解决根问题。
2. 对齐被忽略
如果块大小和类型对齐要求不匹配,很容易埋下未定义行为。
3. 回收顺序与生命周期不清晰
对象还在被引用,就提前放回池里,这类 bug 会比普通野指针更难查。
4. 把内存池当银弹
很多性能问题本质上是对象设计太碎、拷贝太多、缓存局部性太差。内存池能救一部分,但不能替代整体设计优化。
总结
C++ 性能优化里,内存分配一直是个绕不开的话题。通用分配器很强,但你的业务未必需要那么通用。
当 profile 已经明确告诉你热点在频繁分配和释放上时,一个设计克制的内存池,往往比继续抠函数内几行逻辑更有效。
前提也很明确:先测量,再优化;先确认场景,再决定是否值得引入复杂度。
性能优化最怕的是凭感觉动刀,profile 过的数据,才配得上复杂实现。