在高性能计算、游戏开发或嵌入式系统中,频繁的内存分配与释放往往成为瓶颈。传统的 new/delete 或 malloc/free 调用会导致大量的堆碎片,增加系统调用开销,降低缓存命中率。为了解决这些问题,程序员常常自行实现一个内存池(Memory Pool),通过预分配一大块连续内存,并在此块中按需分配小块,以实现高效的内存管理。以下内容将从理论到实践,详细介绍 C++20 及以上标准下自定义内存池的实现思路、关键技术点以及常见优化技巧。
1. 内存池的基本概念
- 预分配(Pre-allocation):一次性从系统申请一块大内存(如 4 MB 或 64 MB),减少系统调用次数。
- 块划分(Block subdivision):将预分配的大块划分为若干固定大小或可变大小的子块,以满足不同对象的需求。
- 空闲链表(Free-list):维护一条链表记录当前未被占用的子块,分配时从链表头取一个子块,释放时将子块返回链表。
2. 简单实现:固定大小块池
#include <cstddef>
#include <new>
#include <vector>
#include <stdexcept>
class FixedBlockPool {
public:
explicit FixedBlockPool(std::size_t blockSize, std::size_t blockCount)
: blockSize_(blockSize), poolSize_(blockSize * blockCount)
{
pool_ = ::operator new(poolSize_, std::nothrow);
if (!pool_) throw std::bad_alloc();
// 初始化空闲链表
for (std::size_t i = 0; i < blockCount; ++i) {
void* block = static_cast<char*>(pool_) + i * blockSize_;
freeList_.push_back(block);
}
}
~FixedBlockPool() {
::operator delete(pool_, poolSize_);
}
void* allocate() {
if (freeList_.empty())
throw std::bad_alloc(); // 或者扩容
void* block = freeList_.back();
freeList_.pop_back();
return block;
}
void deallocate(void* ptr) {
freeList_.push_back(ptr);
}
private:
std::size_t blockSize_;
std::size_t poolSize_;
void* pool_;
std::vector<void*> freeList_;
};
使用示例
FixedBlockPool intPool(sizeof(int), 1024);
int* p = static_cast<int*>(intPool.allocate());
*p = 42;
intPool.deallocate(p);
3. 动态大小块池:分区技术
当对象大小不固定时,固定大小块池会导致内部碎片。常见方案是将内存池分为若干分区(size classes),每个分区对应一个固定大小块池。按需选择最近的大小类进行分配。
struct SizeClass {
std::size_t blockSize;
std::vector<void*> freeList;
};
class DynamicPool {
public:
DynamicPool(const std::vector<std::size_t>& classes) {
for (auto sz : classes) {
classes_.push_back({sz, {}});
}
}
void* allocate(std::size_t size) {
for (auto& cls : classes_) {
if (size <= cls.blockSize) {
if (cls.freeList.empty()) expand(cls);
void* block = cls.freeList.back();
cls.freeList.pop_back();
return block;
}
}
// 超过最大块大小,退回标准堆
return ::operator new(size);
}
void deallocate(void* ptr, std::size_t size) {
for (auto& cls : classes_) {
if (size <= cls.blockSize) {
cls.freeList.push_back(ptr);
return;
}
}
::operator delete(ptr);
}
private:
void expand(SizeClass& cls) {
// 简单实现:一次扩展 64 个块
std::size_t num = 64;
void* pool = ::operator new(num * cls.blockSize);
for (std::size_t i = 0; i < num; ++i) {
void* block = static_cast<char*>(pool) + i * cls.blockSize;
cls.freeList.push_back(block);
}
}
std::vector <SizeClass> classes_;
};
4. 内存池的线程安全
在多线程环境下,最常见的做法是为每个线程维护一个线程本地内存池(TLS),减少锁竞争。C++20 的 std::thread_local 可直接实现:
thread_local FixedBlockPool threadPool(sizeof(MyObject), 512);
如果必须共享同一内存池,则需要使用 std::mutex 或更细粒度的锁(如 std::shared_mutex 或自旋锁):
#include <shared_mutex>
class ThreadSafePool {
public:
void* allocate() {
std::unique_lock lock(mutex_);
return pool_.allocate();
}
void deallocate(void* ptr) {
std::unique_lock lock(mutex_);
pool_.deallocate(ptr);
}
private:
FixedBlockPool pool_{sizeof(MyObject), 2048};
std::shared_mutex mutex_;
};
5. 性能测评与优化技巧
- 避免碎片:分区技术、可变块池结合使用,可降低碎片率。
- 缓存友好:将块的对齐(
alignas)设为 CPU 缓存线长度(如 64 B),减少跨行访问。 - 批量释放:将多个释放操作聚合后一次性返回,减少链表操作开销。
- 自适应扩容:根据实时使用率动态调整每个分区的扩容策略,避免频繁的大块分配。
- 内存对齐:使用
std::align或手动对齐,确保 SIMD 或硬件加速指令的正确性。
6. 与标准库协同使用
C++20 引入了 std::pmr(Polymorphic Memory Resources),提供了统一的内存资源接口。你可以直接把自定义内存池包装成 std::pmr::memory_resource,然后让 std::pmr::vector 等容器使用:
#include <memory_resource>
#include <vector>
class MyPool : public std::pmr::memory_resource {
// 重写 is_equal, do_allocate, do_deallocate, do_protect
};
std::pmr::memory_resource* mr = new MyPool(...);
std::pmr::vector <int> vec(mr);
7. 实际应用案例
- 游戏引擎:在实体组件系统(ECS)中,实体生命周期短、数量多,使用内存池能显著减少 GC 或堆碎片。
- 网络服务器:请求包、缓冲区经常按固定大小分配,内存池可减少系统调用。
- 嵌入式系统:内存资源有限,使用内存池能确保实时性和可预测性。
8. 小结
自定义内存池是 C++ 性能优化的重要手段之一。通过预分配、块划分、空闲链表和分区技术,可以显著降低内存分配成本、减少碎片并提高缓存友好性。结合线程本地存储和 C++20 的 std::pmr,可以实现线程安全、可插拔的内存资源,满足各种高性能场景的需求。希望本篇文章能为你在项目中实现高效内存池提供思路与参考。