在高性能系统中,频繁的new/delete往往会导致大量的碎片化和内存碎片,进而影响缓存命中率、产生不必要的系统调用。为了解决这一问题,开发者常常采用自定义内存池(Memory Pool)来统一管理一块连续的内存区域,并在此区域内部按需分配和回收内存。本文将介绍一种简单且可扩展的内存池实现方式,并讨论其在多线程环境中的应用和优化思路。
1. 设计目标
- 高效分配:分配和回收操作的时间复杂度尽量为 O(1)。
- 内存对齐:支持用户指定的对齐方式,满足结构体对齐需求。
- 可扩展性:当池空间不足时,能够自动扩容。
- 线程安全:多线程环境下能够安全使用。
2. 基本思路
我们采用 固定块大小分配(Fixed‑Size Block Allocation)结合 链表管理 的方式。每个块大小由用户在创建池时指定。池内部维护一个空闲块链表,分配时从链表头取块,释放时将块返回链表。
struct Block {
Block* next;
};
在池初始化时,预先将整个内存区划分为若干块,并将所有块链接起来,形成一个空闲链表。
3. 代码实现
#include <cstddef>
#include <cstdlib>
#include <mutex>
#include <vector>
#include <stdexcept>
class MemoryPool {
public:
MemoryPool(std::size_t blockSize, std::size_t blockCount, std::size_t alignment = alignof(std::max_align_t))
: blockSize_(alignUp(blockSize, alignment)),
blockCount_(blockCount),
alignment_(alignment)
{
poolSize_ = blockSize_ * blockCount_;
pool_ = std::malloc(poolSize_);
if (!pool_) throw std::bad_alloc();
// 初始化空闲链表
freeList_ = reinterpret_cast<Block*>(pool_);
Block* cur = freeList_;
for (std::size_t i = 1; i < blockCount_; ++i) {
cur->next = reinterpret_cast<Block*>(
reinterpret_cast<char*>(pool_) + i * blockSize_);
cur = cur->next;
}
cur->next = nullptr;
}
~MemoryPool() { std::free(pool_); }
void* allocate() {
std::lock_guard<std::mutex> lock(mutex_);
if (!freeList_) {
// 池已满,进行扩容
expandPool();
}
Block* block = freeList_;
freeList_ = freeList_->next;
return block;
}
void deallocate(void* ptr) {
std::lock_guard<std::mutex> lock(mutex_);
reinterpret_cast<Block*>(ptr)->next = freeList_;
freeList_ = reinterpret_cast<Block*>(ptr);
}
private:
std::size_t alignUp(std::size_t size, std::size_t align) {
return (size + align - 1) & ~(align - 1);
}
void expandPool() {
std::size_t newBlockCount = blockCount_ * 2;
std::size_t newPoolSize = blockSize_ * newBlockCount;
void* newPool = std::realloc(pool_, newPoolSize);
if (!newPool) throw std::bad_alloc();
// 重新连接新的块
Block* newFree = reinterpret_cast<Block*>(
reinterpret_cast<char*>(newPool) + blockCount_ * blockSize_);
for (std::size_t i = 1; i < newBlockCount - blockCount_; ++i) {
newFree[i-1].next = reinterpret_cast<Block*>(
reinterpret_cast<char*>(newPool) + (blockCount_ + i) * blockSize_);
}
newFree[newBlockCount - blockCount_ - 1].next = freeList_;
freeList_ = newFree;
pool_ = newPool;
poolSize_ = newPoolSize;
blockCount_ = newBlockCount;
}
std::size_t blockSize_;
std::size_t blockCount_;
std::size_t alignment_;
std::size_t poolSize_;
void* pool_;
Block* freeList_;
std::mutex mutex_;
};
关键点说明
- 对齐:使用
alignUp将块大小向上取整到对齐值,确保每块地址满足对齐要求。 - 扩容:在池满时,使用
realloc扩大内存区,然后把新增的块链接进空闲链表。扩容频率可以通过策略调整,例如仅在块数达到一定阈值后才扩容。 - 线程安全:使用
std::mutex保护分配与释放操作。若对性能要求极高,可采用std::atomic或分区池(per‑thread)来降低锁竞争。
4. 在多线程中的优化
-
分区池(Thread‑Local Pool)
每个线程维护自己的内存池,减少锁竞争。全局池仅在跨线程分配时使用。 -
无锁实现
对链表使用std::atomic<Block*>,实现无锁的 pop/push。适合对延迟极低的场景。 -
预分配大块
对于极大对象(> 1 MB)可直接使用std::malloc,不放入固定块池,以避免大块内存碎片。
5. 使用示例
int main() {
// 每个块 256 字节,初始 1024 块
MemoryPool pool(256, 1024);
// 分配 10 次
std::vector<void*> ptrs;
for (int i = 0; i < 10; ++i)
ptrs.push_back(pool.allocate());
// 释放
for (void* p : ptrs)
pool.deallocate(p);
}
6. 进一步的改进
- 内存统计:加入统计接口,查看已用块数、剩余块数等。
- 内存泄漏检测:在析构时检查
freeList_是否为空。 - 多尺寸支持:使用多个不同大小的子池,或实现分层内存池(Small‑Object Allocator)。
结语
自定义内存池可以显著提升 C++ 程序在高并发、低延迟场景下的性能。通过固定块大小、链表管理以及必要的线程安全措施,我们可以得到一个简洁而高效的实现。根据业务场景进一步扩展功能,如分区池、无锁实现等,可使内存池更加适配复杂系统需求。