在 C++ 开发中,内存分配往往是性能瓶颈的关键点之一。虽然标准库提供了 new/delete、malloc/free 等通用分配器,但在特定场景下,使用自定义分配器可以显著提升程序的速度与内存利用率。本文将系统介绍如何在 C++ 中实现一个高效的自定义内存分配器,包括设计原则、实现细节以及常见的性能优化手段。
1. 自定义分配器的设计目标
| 目标 | 说明 |
|---|---|
| 速度 | 分配和释放操作比标准分配器更快 |
| 内存碎片化最小化 | 通过块池、内存对齐等手段减少碎片 |
| 线程安全 | 支持多线程并发访问时不产生竞争 |
| 可配置 | 允许用户指定块大小、池大小、回收策略等 |
实现自定义分配器时,需先明确使用场景:是否需要单线程、是否对对齐有特殊要求、是否需要对象池等。不同场景会影响分配器的内部结构。
2. 基本实现思路
自定义分配器通常采用 内存池(Memory Pool)技术。核心思路是预先申请一大块连续内存,然后在这块内存上切分出若干固定大小或可变大小的块,最后通过链表或位图管理空闲块。下面给出一个最小可行示例,演示固定大小块的分配器。
2.1 结构体定义
struct PoolBlock {
PoolBlock* next;
};
next指向下一个空闲块,实现一个自由链表。
2.2 分配器类
class FixedSizeAllocator {
public:
FixedSizeAllocator(std::size_t blockSize, std::size_t poolSize);
~FixedSizeAllocator();
void* allocate();
void deallocate(void* ptr);
private:
std::size_t blockSize_;
std::size_t poolSize_;
void* poolStart_;
PoolBlock* freeList_;
};
blockSize_:每个块的大小,必须满足对齐需求。poolSize_:池中块的数量。poolStart_:原始内存块起始地址。freeList_:自由链表头指针。
2.3 构造函数实现
FixedSizeAllocator::FixedSizeAllocator(std::size_t blockSize, std::size_t poolSize)
: blockSize_(blockSize),
poolSize_(poolSize),
poolStart_(nullptr),
freeList_(nullptr) {
// 为块对齐
std::size_t alignedSize = (blockSize_ + sizeof(void*) - 1) & ~(sizeof(void*) - 1);
// 预先申请内存
poolStart_ = std::malloc(alignedSize * poolSize_);
if (!poolStart_) throw std::bad_alloc();
// 初始化自由链表
char* ptr = static_cast<char*>(poolStart_);
for (std::size_t i = 0; i < poolSize_; ++i) {
PoolBlock* block = reinterpret_cast<PoolBlock*>(ptr);
block->next = freeList_;
freeList_ = block;
ptr += alignedSize;
}
}
2.4 分配/释放实现
void* FixedSizeAllocator::allocate() {
if (!freeList_) return nullptr; // 或者扩容
PoolBlock* block = freeList_;
freeList_ = freeList_->next;
return block;
}
void FixedSizeAllocator::deallocate(void* ptr) {
if (!ptr) return;
PoolBlock* block = reinterpret_cast<PoolBlock*>(ptr);
block->next = freeList_;
freeList_ = block;
}
2.5 析构函数
FixedSizeAllocator::~FixedSizeAllocator() {
std::free(poolStart_);
}
这样,一个最小的固定大小块分配器就完成了。使用时可以:
FixedSizeAllocator pool( sizeof(MyObject), 1000 );
MyObject* obj = static_cast<MyObject*>(pool.allocate());
new (obj) MyObject(); // 位置构造
// 使用完毕后
obj->~MyObject(); // 手动析构
pool.deallocate(obj);
3. 性能优化
-
对齐优化
- 对齐要求通常为
max_align_t或类型自身对齐。使用std::max_align_t或alignas可以保证兼容性。
- 对齐要求通常为
-
避免多线程竞争
- 单线程场景无需锁。多线程场景可以采用 线程本地分配器(Thread‑Local Allocation Buffer, TLAB),每个线程拥有自己的小池,减少锁竞争。
-
回收策略
- 若频繁释放对象,可以在分配器内部维护 回收池,按一定策略批量回收,降低系统调用开销。
-
扩容机制
- 当自由链表耗尽时,可按需动态扩容。注意扩容的线程安全与碎片管理。
-
缓存亲和性
- 对于 NUMA 系统,最好让每个节点拥有自己的内存池,以减少跨节点访问延迟。
4. 进阶实现:可变大小块分配器
固定大小块分配器对大多数情况已足够,但若需要支持不同大小对象,可采用 Buddy 系统 或 Free List + Bit Map 的组合:
- Buddy 系统:将整个池按 2 的幂次拆分。分配时递归拆分,释放时合并。
- Free List + Bit Map:维护多层自由链表,每层对应一种块大小,使用位图快速定位可用块。
实现细节会更复杂,但仍以块池为核心,核心思路不变。
5. 与标准分配器的集成
C++20 引入了 std::pmr::memory_resource,可以轻松自定义分配器并与 STL 容器配合。实现一个继承自 std::pmr::memory_resource 的自定义池:
class PmrPool : public std::pmr::memory_resource {
// 继承并实现 do_allocate / do_deallocate / do_is_equal
};
然后:
std::pmr::vector <int> vec{ std::pmr::polymorphic_allocator<int>{&pool} };
这样,你可以在保持 STL 兼容性的同时,利用自定义内存池带来的性能优势。
6. 小结
- 自定义内存分配器通过预先申请大块内存并使用自由链表/位图管理空闲块,可显著降低
new/delete的系统调用开销。 - 设计时需关注块大小、对齐、线程安全与扩容策略,确保在目标环境中的高效运行。
- 对于可变大小对象,Buddy 系统或多层自由链表是常见的实现方案。
- C++20 的
std::pmr生态使得自定义分配器可以无缝集成到 STL 容器中。
在实际项目中,你可以根据具体需求选择固定大小或可变大小分配器,并结合线程本地化和回收策略进一步优化。通过合理设计,内存分配器往往能成为提升整体性能的关键组件。