30、C++项目实战:内存池实现
内存池,说白了就是自己管理内存,而不是每次都找操作系统要。我刚开始做C++服务端开发时,遇到一个高并发的消息转发模块,每秒要创建销毁几万个对象。用默认的new/delete?结果就是程序跑着跑着越来越卡,CPU全耗在内存分配上了。后来我亲手写了个内存池,性能直接提升了三倍。嗯,今天我们就来聊聊这个。
为什么需要内存池?
你想想看,每次调用new,底层其实要走系统调用(malloc -> brk/mmap)。系统调用有多慢?大概比普通函数调用慢几十倍。而且频繁分配小块内存,还会导致内存碎片——明明内存还剩很多,但就是分配不出连续的大块。
内存池的思路很简单:一次性向系统申请一大块内存,然后自己切成小块来分配。释放的时候也不还给系统,而是放回池子里复用。这样既避免了系统调用,又消除了碎片。
内存池的两种常见模型
我个人习惯把内存池分成两类:
- 固定大小内存池:每个块大小相同,适合对象池场景
- 可变大小内存池:支持不同大小的分配请求,类似malloc的替代品
今天我们先讲固定大小的。为什么?因为它在实际项目中用得最多,而且实现起来最清晰。我记得在某个游戏项目中,所有子弹对象都是用固定大小内存池管理的,效果非常好。
核心数据结构
固定大小内存池的核心,是一个空闲链表。每个空闲块里存着下一个空闲块的地址。分配时从链表头取一个,释放时再插回去。
// 内存块头部(复用空闲块的内存来存指针)
struct BlockHeader {
BlockHeader* next;
};
class FixedMemoryPool {
private:
BlockHeader* freeList_; // 空闲链表头
char* pool_; // 实际内存池起始地址
size_t blockSize_; // 每个块的大小
size_t blockCount_; // 块的总数
public:
FixedMemoryPool(size_t blockSize, size_t blockCount);
~FixedMemoryPool();
void* allocate();
void deallocate(void* ptr);
};
实现细节
构造函数里,我们一次性申请一大块内存,然后初始化空闲链表:
FixedMemoryPool::FixedMemoryPool(size_t blockSize, size_t blockCount)
: blockSize_(blockSize), blockCount_(blockCount) {
// 每个块至少能存一个指针
if (blockSize_ < sizeof(BlockHeader)) {
blockSize_ = sizeof(BlockHeader);
}
// 申请整块内存
pool_ = new char[blockSize_ * blockCount_];
// 初始化空闲链表
freeList_ = reinterpret_cast<BlockHeader*>(pool_);
BlockHeader* current = freeList_;
for (size_t i = 1; i < blockCount_; ++i) {
BlockHeader* next = reinterpret_cast<BlockHeader*>(pool_ + i * blockSize_);
current->next = next;
current = next;
}
current->next = nullptr; // 最后一个块指向空
}
分配和释放的代码非常简洁:
void* FixedMemoryPool::allocate() {
if (freeList_ == nullptr) {
return nullptr; // 池子用完了
}
BlockHeader* block = freeList_;
freeList_ = freeList_->next;
return block; // 返回这块内存给用户
}
void FixedMemoryPool::deallocate(void* ptr) {
if (ptr == nullptr) return;
// 把这块内存插回空闲链表头部
BlockHeader* block = static_cast<BlockHeader*>(ptr);
block->next = freeList_;
freeList_ = block;
}
内存池的完整结构图
下面这张图展示了固定大小内存池的完整工作流程:
避坑指南
我曾经在一个网络库里用过内存池,踩过几个坑,分享给你:
- 内存对齐问题:有些硬件平台要求数据按4字节或8字节对齐。如果池子里的块不对齐,访问时会崩溃。解决办法:在构造函数里把
blockSize_向上对齐到alignof(std::max_align_t)。 - 线程安全:上面的实现不是线程安全的。多线程环境下,分配和释放需要加锁。我一般用
std::mutex或者无锁链表(CAS操作)来解决。 - 池子耗尽:当所有块都用完时,
allocate()返回nullptr。调用方必须检查返回值。我习惯的做法是:池子耗尽时自动扩容——再申请一块新内存,挂到空闲链表后面。
性能对比
我做过一个简单的基准测试,分配释放100万次:
| 分配方式 | 耗时(毫秒) | 内存碎片 |
|---|---|---|
| new/delete | 约 850ms | 严重 |
| 内存池(固定大小) | 约 45ms | 无 |
看到差距了吧?快了将近20倍。而且内存池的分配时间几乎是常数,不会像系统分配那样越跑越慢。
总结
内存池是C++高性能编程的必备技能。它的核心就三点:预分配、空闲链表、复用。实现起来不过几十行代码,但带来的性能提升是巨大的。
嗯,今天的分享就到这里。如果你在实际项目中遇到内存池相关的问题,欢迎交流。记住:能用内存池的地方,就别用new/delete。