尊敬的各位技术同行,
欢迎来到今天的技术讲座。今天我们将深入探讨一个在高性能、实时系统开发中至关重要的话题:内存管理。特别地,我们将聚焦于如何利用 C++ 语言编写自定义的内存池(Memory Pool),以彻底消除在传统动态内存分配机制下可能产生的垃圾回收(GC)压力,进而保障实时任务的确定性(determinism)和低延迟。
在许多对响应时间有严格要求的应用场景,例如游戏引擎、嵌入式系统、高频交易、航空航天控制、音视频处理等,哪怕是微秒级的延迟抖动都可能导致严重的后果。C++ 因其直接操作内存的能力和零开销抽象的特性,成为这些领域首选的编程语言。然而,即使在 C++ 中,我们默认使用的 new 和 delete 操作符(底层通常调用 malloc 和 free)也并非没有代价。它们虽然不像 Java 或 C# 等语言那样有显式的垃圾回收器,但其内部的堆管理机制同样可能引入不确定性延迟和内存碎片,这正是我们今天需要解决的核心问题。
内存分配的本质与传统机制的挑战
要理解内存池的价值,我们首先需要回顾一下传统的动态内存分配机制 (new/delete 或 malloc/free) 是如何工作的,以及它们为何会在实时系统中造成“GC压力”——尽管 C++ 没有显式 GC。
当程序请求动态内存时,例如 int* p = new int; 或 void* mem = malloc(sizeof(int));,操作系统或C运行时库的堆管理器会执行一系列复杂的操作:
- 查找空闲块: 堆管理器需要遍历其内部维护的空闲内存块列表,寻找一个足够大的块来满足请求。这个过程可能从头到尾扫描,或者使用更复杂的算法(如位图、红黑树等),其时间复杂度通常不是 O(1)。
- 分割空闲块: 如果找到的空闲块大于请求的大小,堆管理器会将其分割,一部分分配给程序,另一部分作为新的空闲块放回列表中。
- 更新元数据: 分配的内存块通常会在其头部或尾部附加一些元数据(如块大小、是否空闲等),以便在释放时进行管理。
- 同步机制: 在多线程环境中,堆管理器需要使用锁(互斥量)来保护其内部数据结构,以防止并发访问导致的数据损坏。这会引入线程争用和上下文切换的开销。
- 系统调用: 如果堆中没有足够的内存,堆管理器可能需要向操作系统发起系统调用(如 Linux 上的
mmap或sbrk,Windows 上的VirtualAlloc)来获取更多的内存页。系统调用是昂贵的,会导致用户态到内核态的切换。
当程序释放内存时,例如 delete p; 或 free(mem);,堆管理器需要将该块标记为空闲,并尝试与相邻的空闲块进行合并( coalescing),以减少内存碎片。这个过程同样需要查找、更新元数据和同步。
传统动态内存分配的固有挑战:
- 不确定性延迟(Non-deterministic Latency):
- 查找空闲块的时间取决于堆的状态(空闲块的数量、大小分布)。
- 合并空闲块的时间也同样不确定。
- 线程锁竞争会导致不可预测的等待。
- 系统调用更是高延迟操作。
- 这些因素使得
new和delete的执行时间不可预测,可能在某一时刻突然飙升,从而破坏实时系统的关键时间约束。这就是我们所说的“GC压力”的本质——虽然不是Java/C#那种显式GC,但其效果是类似的:在不可预测的时间点,程序会因内存管理而暂停或减速。
- 内存碎片(Memory Fragmentation):
- 外部碎片: 随着程序的运行,内存不断分配和释放,堆中会出现许多小的、不连续的空闲块。即使总的空闲内存足够,也可能因为没有一个连续的块足够大而导致分配失败。
- 内部碎片: 堆管理器为了简化管理,可能会以固定大小的倍数(如8字节、16字节)来分配内存,或者在分配的块中包含元数据。这导致实际分配的内存可能比请求的稍大,未使用的部分就是内部碎片。
- 内存碎片会降低内存利用率,并可能导致分配失败,甚至需要更频繁地向操作系统请求内存。
- 分配/释放开销(Overhead):
- 除了上述的时间开销,每个分配的内存块都需要存储额外的元数据,这增加了内存本身的开销。
在实时系统中,我们追求的是确定性行为和可预测的性能。传统的堆分配机制显然与这些目标背道而驰。因此,为了消除这些不确定性因素,自定义内存池应运而生。
内存池的优势与核心理念
内存池的核心思想是:程序在启动时或任务初始化阶段,一次性向操作系统申请一大块连续的内存,然后自己管理这块内存的分配和释放。 这样一来,程序就不需要频繁地与操作系统或 C 运行时库的堆管理器交互,从而避免了传统分配机制带来的所有问题。
内存池带来的优势:
- 确定性分配与释放(Deterministic Allocation/Deallocation):
- 通过简单的链表操作(例如,从一个预先链接好的空闲块链表中取出或放回一个块),分配和释放操作可以达到 O(1) 的时间复杂度,这使得它们的执行时间高度可预测。
- 消除内存碎片(Reduced Fragmentation):
- 如果内存池设计为分配固定大小的块,可以完全消除外部碎片。
- 通过精心的设计,也可以最小化内部碎片。
- 即使是变长分配的内存池,也可以通过不同的策略(如伙伴系统、Slab 分配)来有效管理碎片。
- 减少系统调用(Minimized System Calls):
- 只需在初始化时进行一次或几次大的系统调用,后续的内存操作都在用户态完成,极大地降低了上下文切换的开销。
- 改善缓存局部性(Improved Cache Locality):
- 由于内存池通常提供连续的内存区域,如果应用程序倾向于分配相关联的对象,它们很可能会被放置在物理上相邻的内存位置。这有助于提高 CPU 缓存命中率,从而提升整体性能。
- 定制化与优化(Customization and Optimization):
- 可以根据应用程序的特定需求(如对象大小分布、生命周期)来设计最适合的分配策略,实现比通用堆管理器更高的效率。
- 更易于调试(Easier Debugging):
- 自定义内存池可以内置额外的调试功能,如内存泄漏检测、使用后释放(use-after-free)检测、越界访问检测等,这些功能在传统堆分配中可能需要借助外部工具。
内存池的设计考量
设计一个高效且健壮的内存池并非简单,需要综合考虑多种因素。
1. 分配策略:固定大小 vs. 可变大小
- 固定大小内存池(Fixed-Size Memory Pool):
- 特点: 专门用于分配单一尺寸的对象。
- 优点: 设计和实现最简单,分配和释放速度最快(O(1)),完全消除外部碎片,内部碎片也最小(如果对象大小与块大小完全匹配)。
- 缺点: 只能用于已知大小的特定类型对象。如果需要分配多种大小的对象,则需要为每种大小创建一个单独的池,或者使用多个固定大小池的组合。
- 适用场景: 大量创建和销毁相同大小对象的场景,如链表节点、树节点、游戏实体、网络数据包等。
- 可变大小内存池(Variable-Size Memory Pool):
- 特点: 可以分配任意大小的内存块。
- 优点: 灵活性高,可以替代
malloc/free。 - 缺点: 实现复杂,分配和释放可能不是 O(1),可能会产生内部和外部碎片。常见的实现策略包括:
- 多固定大小池: 维护多个固定大小的内存池,每个池处理一个预设的块大小。当请求分配时,选择最小的、能满足需求的池。
- 伙伴系统(Buddy System): 将内存块递归地分割成两半,直到满足请求大小。释放时,如果相邻的“伙伴”块也空闲,则合并它们。
- Slab 分配器(Slab Allocator): 针对内核对象设计的,它将内存划分为不同的“slab”,每个slab包含多个相同大小的对象。当一个对象被释放时,它仍然保留在slab中,等待下次使用。
- 适用场景: 对通用内存分配有性能要求,但又无法预知所有对象大小的场景。
本讲座将主要聚焦于固定大小内存池的实现,因为它最直接地体现了内存池的核心优势,并且是构建更复杂通用内存池的基础。我们也会探讨如何将多个固定大小池组合成一个通用的分配器。
2. 块管理
- 空闲链表(Free List): 最常见的管理固定大小内存块的方法。将所有空闲块通过指针链接起来,形成一个链表。分配时从链表头部取走一个块,释放时将块放回链表头部(LIFO,后进先出)或尾部(FIFO,先进先出)。LIFO 通常更优,因为它倾向于重用最近释放的内存,有助于缓存局部性。
3. 线程安全
- 单线程: 最简单,不需要任何同步机制。
- 多线程:
- 全局锁: 使用
std::mutex保护整个内存池,每次分配/释放都需要加锁。简单但可能成为性能瓶颈。 - 线程局部池(Thread-Local Pool): 每个线程拥有自己的内存池。分配/释放操作在线程内部进行,无需加锁。当线程的池耗尽时,可以向全局池请求一个大块,或者从其他线程的池“借用”。这种方式并发性最高。
- 无锁(Lock-Free)数据结构: 使用原子操作和 CAS (Compare-And-Swap) 等技术实现无锁的空闲链表。实现复杂,但可以提供极低的延迟。
- 全局锁: 使用
4. 内存增长策略
- 预分配(Pre-allocation): 在程序启动时一次性分配所有可能需要的内存。简单,但可能浪费内存。
- 按需增长(Allocate-on-Demand / Chunking): 初始分配一小块,当内存不足时,再向操作系统申请另一大块。这避免了过度预分配的内存浪费,但每次增长仍涉及系统调用。
- 不可增长: 严格限制内存使用,一旦耗尽则报错。适用于内存受限的嵌入式系统。
5. 内存对齐(Alignment)
- 现代 CPU 对数据访问有对齐要求,特别是对于 SIMD 指令或某些数据类型(如
double、long long、std::max_align_t)。未对齐的访问可能导致性能下降甚至程序崩溃。内存池必须确保它返回的内存地址是正确对齐的。
6. 放置 new (Placement New)
- C++ 中的
placement new允许在已分配的内存上构造对象,而不是在堆上分配新的内存。这是将内存池与 C++ 对象生命周期管理的桥梁。// 语法: new (address) Type(arguments); void* buffer = myPool->allocate(sizeof(MyObject)); MyObject* obj = new (buffer) MyObject(arg1, arg2); // 在buffer处构造MyObject // ... 使用 obj ... obj->~MyObject(); // 手动调用析构函数 myPool->deallocate(obj); // 释放内存回池
实现一个简单的固定大小内存池
让我们从一个最简单、单线程、不可增长的固定大小内存池开始。这个内存池将一次性分配一个大块内存,并将其切割成相同大小的小块,通过一个空闲链表进行管理。
核心思想
- 内存块结构: 每个小块既可以存储用户数据,也可以在空闲时存储指向下一个空闲块的指针。为了实现这个目的,我们可以使用
union。 - 空闲链表: 一个指针
_freeListHead指向空闲链表的第一个块。 - 初始化: 在构造函数中,一次性申请一大块原始内存,然后将其分割成
blockSize大小的块,并将所有这些块链接起来,形成空闲链表。 - 分配: 从
_freeListHead取出第一个块,更新_freeListHead指向下一个块。 - 释放: 将释放的块放回
_freeListHead的位置。
代码实现
#include <cstddef> // For std::size_t
#include <new> // For placement new
#include <stdexcept> // For std::bad_alloc
#include <iostream>
// 为了确保内存对齐,我们可能需要一个结构来表示链表节点,
// 并且这个结构的大小要足以容纳任何可能的指针。
// 同时,为了满足用户数据对齐要求,实际分配的内存块需要进一步处理。
// 辅助结构:用于在空闲链表中链接块
// 我们使用union来确保这个FreeBlockHeader的大小至少是指针大小,
// 这样当它作为数据块时,不会覆盖下一个FreeBlockHeader的指针。
// 同时,为了最大化对齐,我们使用alignas(std::max_align_t)
struct FreeBlockHeader
{
FreeBlockHeader* next;
// 考虑极端情况,如果next指针存储在FreeBlockHeader的开头,
// 并且用户请求的blockSize很小,可能无法满足对齐。
// 更好的做法是,FreeBlockHeader只用于链接,实际分配的块是用户请求的。
// 但是对于固定大小池,我们让FreeBlockHeader成为分配块的一部分,
// 并且它的地址就是用户数据的地址。
// 因此,我们需要确保它本身就是对齐的。
};
// FixedSizeMemoryPool 类定义
class FixedSizeMemoryPool
{
public:
FixedSizeMemoryPool(std::size_t blockSize, std::size_t numBlocks);
~FixedSizeMemoryPool();
// 禁止拷贝和移动,因为涉及原始内存管理
FixedSizeMemoryPool(const FixedSizeMemoryPool&) = delete;
FixedSizeMemoryPool& operator=(const FixedSizeMemoryPool&) = delete;
FixedSizeMemoryPool(FixedSizeMemoryPool&&) = delete;
FixedSizeMemoryPool& operator=(FixedSizeMemoryPool&&) = delete;
void* allocate();
void deallocate(void* ptr);
std::size_t getBlockSize() const { return _actualBlockSize; }
std::size_t getCapacity() const { return _numBlocks; }
std::size_t getFreeBlocksCount() const;
private:
char* _memoryBuffer; // 原始内存块
FreeBlockHeader* _freeListHead; // 空闲链表头部
std::size_t _numBlocks; // 块的总数量
std::size_t _blockSize; // 用户请求的块大小
std::size_t _actualBlockSize; // 实际分配的块大小(考虑了链表指针和对齐)
// 辅助函数,确保给定大小的块能够容纳 FreeBlockHeader
// 并且满足最严格的对齐要求
static std::size_t CalculateActualBlockSize(std::size_t requestedSize);
static std::size_t AlignUp(std::size_t size, std::size_t alignment);
};
// 静态辅助函数实现
std::size_t FixedSizeMemoryPool::AlignUp(std::size_t size, std::size_t alignment)
{
return (size + alignment - 1) & ~(alignment - 1);
}
std::size_t FixedSizeMemoryPool::CalculateActualBlockSize(std::size_t requestedSize)
{
// 确保块大小至少能容纳 FreeBlockHeader 的指针,并且能满足最大的对齐要求
// std::max_align_t 是 C++ 标准库中定义的最大对齐类型
const std::size_t headerSize = sizeof(FreeBlockHeader);
const std::size_t alignment = alignof(std::max_align_t); // 或 alignof(FreeBlockHeader)
// 用户请求的大小和FreeBlockHeader大小取最大值,并对齐
std::size_t sizeToHold = std::max(requestedSize, headerSize);
return AlignUp(sizeToHold, alignment);
}
// 构造函数
FixedSizeMemoryPool::FixedSizeMemoryPool(std::size_t blockSize, std::size_t numBlocks)
: _numBlocks(numBlocks), _blockSize(blockSize), _memoryBuffer(nullptr), _freeListHead(nullptr)
{
if (blockSize == 0 || numBlocks == 0)
{
throw std::invalid_argument("Block size and number of blocks must be greater than zero.");
}
// 计算实际分配的块大小,确保能容纳FreeBlockHeader并满足对齐
_actualBlockSize = CalculateActualBlockSize(blockSize);
// 分配一大块连续内存
// 这里使用 new char[],它会调用 malloc。在实际实时系统中,
// 可能需要使用 mmap/VirtualAlloc 来获取页对齐的大块内存。
_memoryBuffer = new char[_actualBlockSize * _numBlocks];
if (!_memoryBuffer)
{
throw std::bad_alloc();
}
// 初始化空闲链表
// 将所有块链接起来
for (std::size_t i = 0; i < _numBlocks; ++i)
{
FreeBlockHeader* currentBlock = reinterpret_cast<FreeBlockHeader*>(_memoryBuffer + i * _actualBlockSize);
currentBlock->next = _freeListHead; // 将当前块指向当前链表头
_freeListHead = currentBlock; // 更新链表头为当前块
}
std::cout << "FixedSizeMemoryPool created: "
<< "Requested Block Size = " << _blockSize << " bytes, "
<< "Actual Block Size (with overhead) = " << _actualBlockSize << " bytes, "
<< "Total Blocks = " << _numBlocks << ", "
<< "Total Memory = " << (_actualBlockSize * _numBlocks) << " bytes." << std::endl;
}
// 析构函数
FixedSizeMemoryPool::~FixedSizeMemoryPool()
{
delete[] _memoryBuffer;
_memoryBuffer = nullptr;
_freeListHead = nullptr; // 指针已经随内存释放而无效
std::cout << "FixedSizeMemoryPool destroyed." << std::endl;
}
// 分配内存
void* FixedSizeMemoryPool::allocate()
{
if (!_freeListHead)
{
// 内存池已满,无法分配
// 在实际应用中,这里可以抛出异常,或返回 nullptr,或尝试扩展内存池
std::cerr << "Error: FixedSizeMemoryPool is out of memory!" << std::endl;
throw std::bad_alloc();
}
void* allocatedBlock = _freeListHead; // 取出链表头
_freeListHead = _freeListHead->next; // 更新链表头指向下一个块
// 注意:这里返回的地址是 FreeBlockHeader 的地址,也是用户数据的起始地址。
// 在构造时,我们已经确保了它对齐,并且大小足够。
return allocatedBlock;
}
// 释放内存
void FixedSizeMemoryPool::deallocate(void* ptr)
{
if (!ptr)
{
return; // 尝试释放空指针
}
// 简单检查 ptr 是否在池的范围内 (可选,但有助于调试)
// 这是一个简单但不完全严格的检查,因为ptr可能指向池内的某个非起始位置。
// 更严格的检查需要记录每个分配块的状态。
char* charPtr = static_cast<char*>(ptr);
if (charPtr < _memoryBuffer || charPtr >= (_memoryBuffer + _actualBlockSize * _numBlocks))
{
std::cerr << "Warning: Attempting to deallocate memory not owned by this pool: " << ptr << std::endl;
// 可以在这里抛出异常或采取其他错误处理措施
return;
}
// 将释放的块放回链表头部
FreeBlockHeader* recycledBlock = static_cast<FreeBlockHeader*>(ptr);
recycledBlock->next = _freeListHead;
_freeListHead = recycledBlock;
}
std::size_t FixedSizeMemoryPool::getFreeBlocksCount() const
{
std::size_t count = 0;
FreeBlockHeader* current = _freeListHead;
while (current != nullptr)
{
count++;
current = current->next;
}
return count;
}
// --- 示例用法 ---
struct MyObject
{
int id;
double value;
char name[20];
MyObject(int i, double v, const char* n) : id(i), value(v)
{
std::strncpy(name, n, sizeof(name) - 1);
name[sizeof(name) - 1] = '';
// std::cout << "MyObject(" << id << ") constructed." << std::endl;
}
~MyObject()
{
// std::cout << "MyObject(" << id << ") destructed." << std::endl;
}
void print() const
{
std::cout << " MyObject: ID=" << id << ", Value=" << value << ", Name=" << name << std::endl;
}
};
int main()
{
// 1. 创建一个固定大小内存池,用于MyObject
// MyObject 的大小是 sizeof(int) + sizeof(double) + sizeof(char[20]) = 4 + 8 + 20 = 32字节
// 但实际分配的块会因为对齐和FreeBlockHeader而更大
const std::size_t objectSize = sizeof(MyObject);
const std::size_t poolCapacity = 10; // 池中可容纳10个MyObject
FixedSizeMemoryPool pool(objectSize, poolCapacity);
// 2. 使用 placement new 从内存池分配和构造对象
MyObject* objects[poolCapacity];
std::cout << "nAllocating and constructing " << poolCapacity << " MyObjects:" << std::endl;
for (int i = 0; i < poolCapacity; ++i)
{
try
{
void* mem = pool.allocate();
objects[i] = new (mem) MyObject(i + 1, (double)(i + 1) * 10.0, ("Object" + std::to_string(i + 1)).c_str());
objects[i]->print();
}
catch (const std::bad_alloc& e)
{
std::cerr << "Failed to allocate object " << i + 1 << ": " << e.what() << std::endl;
objects[i] = nullptr;
}
}
std::cout << "Free blocks left: " << pool.getFreeBlocksCount() << std::endl;
// 尝试分配更多,应该会失败
std::cout << "nAttempting to allocate one more object (should fail):" << std::endl;
try
{
void* mem = pool.allocate();
MyObject* extraObj = new (mem) MyObject(99, 99.9, "Extra");
extraObj->print(); // 这行不应该执行到
extraObj->~MyObject();
pool.deallocate(extraObj);
}
catch (const std::bad_alloc& e)
{
std::cerr << "Caught expected exception: " << e.what() << std::endl;
}
std::cout << "Free blocks left: " << pool.getFreeBlocksCount() << std::endl;
// 3. 手动调用析构函数,并释放内存回池
std::cout << "nDeallocating and destructing MyObjects:" << std::endl;
for (int i = 0; i < poolCapacity; ++i)
{
if (objects[i])
{
objects[i]->~MyObject(); // 必须手动调用析构函数
pool.deallocate(objects[i]); // 将内存归还给内存池
}
}
std::cout << "Free blocks left: " << pool.getFreeBlocksCount() << std::endl;
// 4. 再次分配,验证内存复用
std::cout << "nAllocating again after deallocation:" << std::endl;
MyObject* newObj = nullptr;
try {
void* mem = pool.allocate();
newObj = new (mem) MyObject(100, 100.0, "ReusedObject");
newObj->print();
} catch (const std::bad_alloc& e) {
std::cerr << "Failed to allocate reused object: " << e.what() << std::endl;
}
std::cout << "Free blocks left: " << pool.getFreeBlocksCount() << std::endl;
if (newObj) {
newObj->~MyObject();
pool.deallocate(newObj);
}
std::cout << "Free blocks left: " << pool.getFreeBlocksCount() << std::endl;
return 0;
}
代码解析:
FreeBlockHeader结构: 这是一个关键的设计。它是一个union,包含一个FreeBlockHeader* next指针。当内存块空闲时,它存储下一个空闲块的地址;当内存块被分配用于存储用户数据时,这部分内存就被用户数据覆盖。我们通过static_cast或reinterpret_cast在void*和FreeBlockHeader*之间进行转换。CalculateActualBlockSize确保了每个分配的块大小足以容纳FreeBlockHeader,并且满足std::max_align_t的对齐要求,这通常是所有基本类型中最大的对齐要求。- 构造函数:
- 计算
_actualBlockSize:这是为了确保每个块不仅能存储用户数据,还能在空闲时存储FreeBlockHeader*,并且地址是正确对齐的。 new char[_actualBlockSize * _numBlocks]:一次性从系统堆(或mmap等)申请一大块原始内存。- 循环链接所有块:这是内存池初始化的核心。它遍历
_memoryBuffer,将每个_actualBlockSize大小的子块转换为FreeBlockHeader*,然后通过next指针将它们链接起来,形成一个 LIFO 的空闲链表。
- 计算
allocate():- 检查
_freeListHead是否为空。如果为空,表示池已耗尽,抛出std::bad_alloc。 - 否则,取出
_freeListHead指向的块,并更新_freeListHead指向下一个块。这个操作是 O(1)。
- 检查
deallocate():- 将传入的
ptr转换为FreeBlockHeader*。 - 将这个块的
next指针指向当前的_freeListHead。 - 更新
_freeListHead为这个新释放的块。这个操作也是 O(1)。 - 添加了一个简单的
ptr范围检查,但这并非完全严格。
- 将传入的
main()示例: 演示了如何创建池,使用placement new构造MyObject,以及手动调用析构函数和将内存归还给池。
这个简单内存池的优点是性能极高且确定性。缺点是它不能扩展,且只能分配固定大小的内存块。
增强的固定大小内存池:线程安全与动态增长
在实际应用中,单线程且固定容量的内存池可能不够用。我们需要考虑线程安全和动态增长的能力。
1. 线程安全:使用 std::mutex
最简单的线程安全方案是为内存池的所有操作(allocate 和 deallocate)添加一个互斥锁 std::mutex。
#include <mutex> // For std::mutex
// ... (FixedSizeMemoryPool 的 FreeBlockHeader, CalculateActualBlockSize, AlignUp 保持不变) ...
class ThreadSafeFixedSizeMemoryPool
{
public:
ThreadSafeFixedSizeMemoryPool(std::size_t blockSize, std::size_t numBlocks);
~ThreadSafeFixedSizeMemoryPool();
ThreadSafeFixedSizeMemoryPool(const ThreadSafeFixedSizeMemoryPool&) = delete;
ThreadSafeFixedSizeMemoryPool& operator=(const ThreadSafeFixedSizeMemoryPool&) = delete;
ThreadSafeFixedSizeMemoryPool(ThreadSafeFixedSizeMemoryPool&&) = delete;
ThreadSafeFixedSizeMemoryPool& operator=(ThreadSafeFixedSizeMemoryPool&&) = delete;
void* allocate();
void deallocate(void* ptr);
std::size_t getBlockSize() const { return _actualBlockSize; }
std::size_t getCapacity() const { return _numBlocks; }
std::size_t getFreeBlocksCount() const;
private:
char* _memoryBuffer;
FreeBlockHeader* _freeListHead;
std::size_t _numBlocks;
std::size_t _blockSize;
std::size_t _actualBlockSize;
std::mutex _mutex; // 互斥锁
static std::size_t CalculateActualBlockSize(std::size_t requestedSize);
static std::size_t AlignUp(std::size_t size, std::size_t alignment);
};
// ... (CalculateActualBlockSize, AlignUp 实现与 FixedSizeMemoryPool 相同) ...
ThreadSafeFixedSizeMemoryPool::ThreadSafeFixedSizeMemoryPool(std::size_t blockSize, std::size_t numBlocks)
: _numBlocks(numBlocks), _blockSize(blockSize), _memoryBuffer(nullptr), _freeListHead(nullptr)
{
if (blockSize == 0 || numBlocks == 0)
{
throw std::invalid_argument("Block size and number of blocks must be greater than zero.");
}
_actualBlockSize = CalculateActualBlockSize(blockSize);
_memoryBuffer = new char[_actualBlockSize * _numBlocks];
if (!_memoryBuffer)
{
throw std::bad_alloc();
}
for (std::size_t i = 0; i < _numBlocks; ++i)
{
FreeBlockHeader* currentBlock = reinterpret_cast<FreeBlockHeader*>(_memoryBuffer + i * _actualBlockSize);
currentBlock->next = _freeListHead;
_freeListHead = currentBlock;
}
std::cout << "ThreadSafeFixedSizeMemoryPool created: "
<< "Requested Block Size = " << _blockSize << " bytes, "
<< "Actual Block Size = " << _actualBlockSize << " bytes, "
<< "Total Blocks = " << _numBlocks << ", "
<< "Total Memory = " << (_actualBlockSize * _numBlocks) << " bytes." << std::endl;
}
ThreadSafeFixedSizeMemoryPool::~ThreadSafeFixedSizeMemoryPool()
{
delete[] _memoryBuffer;
_memoryBuffer = nullptr;
_freeListHead = nullptr;
std::cout << "ThreadSafeFixedSizeMemoryPool destroyed." << std::endl;
}
void* ThreadSafeFixedSizeMemoryPool::allocate()
{
std::lock_guard<std::mutex> lock(_mutex); // 加锁
if (!_freeListHead)
{
std::cerr << "Error: ThreadSafeFixedSizeMemoryPool is out of memory!" << std::endl;
throw std::bad_alloc();
}
void* allocatedBlock = _freeListHead;
_freeListHead = _freeListHead->next;
return allocatedBlock;
}
void ThreadSafeFixedSizeMemoryPool::deallocate(void* ptr)
{
std::lock_guard<std::mutex> lock(_mutex); // 加锁
if (!ptr)
{
return;
}
// 可以在这里添加更严格的 ptr 范围检查
char* charPtr = static_cast<char*>(ptr);
if (charPtr < _memoryBuffer || charPtr >= (_memoryBuffer + _actualBlockSize * _numBlocks))
{
std::cerr << "Warning: Attempting to deallocate memory not owned by this pool: " << ptr << std::endl;
return;
}
FreeBlockHeader* recycledBlock = static_cast<FreeBlockHeader*>(ptr);
recycledBlock->next = _freeListHead;
_freeListHead = recycledBlock;
}
std::size_t ThreadSafeFixedSizeMemoryPool::getFreeBlocksCount() const
{
std::lock_guard<std::mutex> lock(_mutex); // 加锁
std::size_t count = 0;
FreeBlockHeader* current = _freeListHead;
while (current != nullptr)
{
count++;
current = current->next;
}
return count;
}
线程安全解析:
std::mutex _mutex;成员变量用于保护_freeListHead和其他内部状态。std::lock_guard<std::mutex> lock(_mutex);在allocate()和deallocate()函数的开头使用,确保这些操作是互斥的。std::lock_guard是一个 RAII(Resource Acquisition Is Initialization)类,它在构造时加锁,在析构时自动解锁,即使函数提前返回或抛出异常也能保证锁被正确释放。- 虽然
std::mutex提供了线程安全,但在高并发场景下,锁竞争可能成为新的性能瓶颈。对于极致的实时性要求,可能需要考虑更复杂的无锁数据结构或线程局部内存池。
2. 动态增长:分块(Chunking)策略
为了让内存池能够按需增长,而不是一次性预分配所有内存,我们可以采用分块(Chunking)策略。内存池不再拥有一个单一的 _memoryBuffer,而是维护一个 std::vector<char*> 来存储多个内存块(chunk)。当当前的 chunk 耗尽时,再向操作系统请求一个新的 chunk。
#include <vector> // For std::vector
// ... (其他头文件和FreeBlockHeader, CalculateActualBlockSize, AlignUp 保持不变) ...
class GrowableFixedSizeMemoryPool
{
public:
GrowableFixedSizeMemoryPool(std::size_t blockSize, std::size_t initialNumBlocks, std::size_t growNumBlocks);
~GrowableFixedSizeMemoryPool();
GrowableFixedSizeMemoryPool(const GrowableFixedSizeMemoryPool&) = delete;
GrowableFixedSizeMemoryPool& operator=(const GrowableFixedSizeMemoryPool&) = delete;
GrowableFixedSizeMemoryPool(GrowableFixedSizeMemoryPool&&) = delete;
GrowableFixedSizeMemoryPool& operator=(GrowableFixedSizeMemoryPool&&) = delete;
void* allocate();
void deallocate(void* ptr);
std::size_t getBlockSize() const { return _actualBlockSize; }
std::size_t getTotalCapacity() const { return _totalNumBlocks; }
std::size_t getFreeBlocksCount() const;
private:
std::vector<char*> _memoryChunks; // 存储所有内存块的指针
FreeBlockHeader* _freeListHead;
std::size_t _blockSize;
std::size_t _actualBlockSize;
std::size_t _initialNumBlocks; // 初始块数量
std::size_t _growNumBlocks; // 每次增长的块数量
std::size_t _totalNumBlocks; // 当前池的总块数量
std::mutex _mutex; // 线程安全
// 辅助函数:分配一个新的内存块并将其添加到空闲链表
void grow();
static std::size_t CalculateActualBlockSize(std::size_t requestedSize);
static std::size_t AlignUp(std::size_t size, std::size_t alignment);
};
// ... (CalculateActualBlockSize, AlignUp 实现与 FixedSizeMemoryPool 相同) ...
GrowableFixedSizeMemoryPool::GrowableFixedSizeMemoryPool(std::size_t blockSize, std::size_t initialNumBlocks, std::size_t growNumBlocks)
: _blockSize(blockSize), _initialNumBlocks(initialNumBlocks), _growNumBlocks(growNumBlocks),
_totalNumBlocks(0), _freeListHead(nullptr)
{
if (blockSize == 0 || initialNumBlocks == 0 || growNumBlocks == 0)
{
throw std::invalid_argument("Block size, initial number of blocks, and grow number of blocks must be greater than zero.");
}
_actualBlockSize = CalculateActualBlockSize(blockSize);
// 初始增长
grow();
std::cout << "GrowableFixedSizeMemoryPool created: "
<< "Requested Block Size = " << _blockSize << " bytes, "
<< "Actual Block Size = " << _actualBlockSize << " bytes, "
<< "Initial Blocks = " << _initialNumBlocks << ", "
<< "Grow Blocks = " << _growNumBlocks << ", "
<< "Total Capacity = " << _totalNumBlocks << " blocks." << std::endl;
}
GrowableFixedSizeMemoryPool::~GrowableFixedSizeMemoryPool()
{
std::lock_guard<std::mutex> lock(_mutex); // 析构时也加锁以防万一
for (char* chunk : _memoryChunks)
{
delete[] chunk;
}
_memoryChunks.clear();
_freeListHead = nullptr;
std::cout << "GrowableFixedSizeMemoryPool destroyed." << std::endl;
}
void GrowableFixedSizeMemoryPool::grow()
{
std::size_t blocksToGrow = (_totalNumBlocks == 0) ? _initialNumBlocks : _growNumBlocks;
char* newChunk = new char[_actualBlockSize * blocksToGrow];
if (!newChunk)
{
throw std::bad_alloc();
}
_memoryChunks.push_back(newChunk);
_totalNumBlocks += blocksToGrow;
// 将新块链接到空闲链表
for (std::size_t i = 0; i < blocksToGrow; ++i)
{
FreeBlockHeader* currentBlock = reinterpret_cast<FreeBlockHeader*>(newChunk + i * _actualBlockSize);
currentBlock->next = _freeListHead;
_freeListHead = currentBlock;
}
std::cout << "Pool grew by " << blocksToGrow << " blocks. New total capacity: " << _totalNumBlocks << std::endl;
}
void* GrowableFixedSizeMemoryPool::allocate()
{
std::lock_guard<std::mutex> lock(_mutex);
if (!_freeListHead)
{
// 尝试增长内存池
try
{
grow(); // 增长操作可能需要向操作系统申请内存,有延迟
}
catch (const std::bad_alloc& e)
{
std::cerr << "Error: GrowableFixedSizeMemoryPool failed to grow: " << e.what() << std::endl;
throw; // 再次抛出,表示分配失败
}
}
void* allocatedBlock = _freeListHead;
_freeListHead = _freeListHead->next;
return allocatedBlock;
}
void GrowableFixedSizeMemoryPool::deallocate(void* ptr)
{
std::lock_guard<std::mutex> lock(_mutex);
if (!ptr)
{
return;
}
// 对于分块池,ptr范围检查更复杂,需要检查是否在任何一个 _memoryChunks 范围内
bool found = false;
char* charPtr = static_cast<char*>(ptr);
for (char* chunk : _memoryChunks)
{
if (charPtr >= chunk && charPtr < (chunk + _actualBlockSize * _growNumBlocks)) // 注意这里使用_growNumBlocks,假设所有chunk大小一致
{
// 还需要进一步验证 ptr 是否是某个块的起始地址
if ((charPtr - chunk) % _actualBlockSize == 0) {
found = true;
break;
}
}
}
if (!found)
{
std::cerr << "Warning: Attempting to deallocate memory not owned by this pool: " << ptr << std::endl;
return;
}
FreeBlockHeader* recycledBlock = static_cast<FreeBlockHeader*>(ptr);
recycledBlock->next = _freeListHead;
_freeListHead = recycledBlock;
}
std::size_t GrowableFixedSizeMemoryPool::getFreeBlocksCount() const
{
std::lock_guard<std::mutex> lock(_mutex);
std::size_t count = 0;
FreeBlockHeader* current = _freeListHead;
while (current != nullptr)
{
count++;
current = current->next;
}
return count;
}
动态增长解析:
_memoryChunks存储了由new char[]分配的各个内存块的基地址。_initialNumBlocks和_growNumBlocks控制池的初始大小和每次增长的大小。grow()方法负责向操作系统申请新的内存(通过new char[]),将其添加到_memoryChunks,然后将新 chunk 中的所有小块链接到_freeListHead。allocate()在发现_freeListHead为空时,会调用grow()来获取更多内存。这意味着第一次grow()操作会引入不确定性延迟,但在实时任务的关键路径上,如果池已预热(即已增长到所需大小),后续的allocate仍是 O(1)。deallocate()的内存范围检查变得更复杂,需要遍历所有_memoryChunks来验证ptr是否属于当前池。
实现一个通用内存池(多固定大小池组合)
对于需要分配多种大小对象的场景,我们可以组合多个固定大小内存池。这种通用内存池的挑战在于:
- 如何根据请求大小选择合适的固定大小池?
- 如何根据释放的地址找到对应的固定大小池?
核心思想
- 池列表: 维护一个
std::vector或std::map,存储不同blockSize的GrowableFixedSizeMemoryPool实例。 - 分配逻辑: 当请求
N字节内存时,查找列表中第一个blockSize大于等于N的池。 - 释放逻辑: 这是最复杂的部分。我们需要知道
ptr最初是从哪个固定大小池分配的。一种常见的方法是在实际分配的内存块头部存储一些元数据,例如它所属的池的 ID 或块的实际大小。
这里我们采用一种简单但有效的元数据存储方式:在用户请求的内存块之前,存储一个表示该块大小的 size_t 值。
代码实现
#include <map> // For std::map
#include <limits> // For std::numeric_limits
// ... (GrowableFixedSizeMemoryPool 的 FreeBlockHeader, CalculateActualBlockSize, AlignUp 保持不变) ...
// 假设 GrowableFixedSizeMemoryPool 及其依赖已定义好
// 为了简化,这里我们直接使用上面的 GrowableFixedSizeMemoryPool
// 并且为了避免循环依赖,我们假设其类定义在上方。
// 实际上,通用池会管理多个这种固定大小池的实例。
class GeneralPurposeMemoryPool
{
public:
// 构造函数:接受一个预设的块大小列表,每个块大小对应一个固定大小池
GeneralPurposeMemoryPool(const std::vector<std::size_t>& blockSizes,
std::size_t initialNumBlocksPerPool,
std::size_t growNumBlocksPerPool);
~GeneralPurposeMemoryPool();
GeneralPurposeMemoryPool(const GeneralPurposeMemoryPool&) = delete;
GeneralPurposeMemoryPool& operator=(const GeneralPurposeMemoryPool&) = delete;
GeneralPurposeMemoryPool(GeneralPurposeMemoryPool&&) = delete;
GeneralPurposeMemoryPool& operator=(GeneralPurposeMemoryPool&&) = delete;
void* allocate(std::size_t size);
void deallocate(void* ptr);
private:
// 存储不同大小的固定大小内存池
// 使用 std::map<size_t, GrowableFixedSizeMemoryPool*> 来按块大小查找池
// 或者使用 std::vector<std::unique_ptr<GrowableFixedSizeMemoryPool>> 和手动查找
// 这里为了简单,我们直接用 map,key 是池管理的实际块大小
std::map<std::size_t, GrowableFixedSizeMemoryPool*> _pools;
// 为了在释放时能根据地址找到对应的池,我们需要存储每个 chunk 的范围信息
// 这是一个简化,更严谨的做法是每个分配的块头部记录其所属池的ID或块大小
// 在这里,我们将分配的实际块大小存储在用户数据的前面
// 实际分配的内存布局: [size_t actual_block_size_for_this_pool | User Data ]
// 定义一个最小的块大小,用于存储元数据
static constexpr std::size_t METADATA_SIZE = sizeof(std::size_t); // 存储实际块大小
// 辅助函数:根据请求大小找到最合适的池
GrowableFixedSizeMemoryPool* findPool(std::size_t requestedSize);
std::mutex _mutex; // 保护 _pools 访问,尤其是在 map 中查找或创建新池时
};
GeneralPurposeMemoryPool::GeneralPurposeMemoryPool(const std::vector<std::size_t>& blockSizes,
std::size_t initialNumBlocksPerPool,
std::size_t growNumBlocksPerPool)
{
if (blockSizes.empty())
{
throw std::invalid_argument("Block sizes list cannot be empty.");
}
// 对块大小进行排序,以便于查找
std::vector<std::size_t> sortedBlockSizes = blockSizes;
std::sort(sortedBlockSizes.begin(), sortedBlockSizes.end());
std::cout << "GeneralPurposeMemoryPool creating with block sizes: ";
for (std::size_t size : sortedBlockSizes) {
// 实际提供给 GrowableFixedSizeMemoryPool 的 blockSize 应该包含 METADATA_SIZE
// 并且要确保能容纳 METADATA_SIZE + requested_size
std::size_t actualSizeForPool = std::max(size + METADATA_SIZE, METADATA_SIZE + sizeof(FreeBlockHeader*)); // 确保最小能存指针
actualSizeForPool = GrowableFixedSizeMemoryPool::AlignUp(actualSizeForPool, alignof(std::max_align_t));
// 避免重复的实际块大小
if (_pools.find(actualSizeForPool) == _pools.end()) {
_pools[actualSizeForPool] = new GrowableFixedSizeMemoryPool(actualSizeForPool, initialNumBlocksPerPool, growNumBlocksPerPool);
std::cout << actualSizeForPool << "(for " << size << ") ";
}
}
std::cout << std::endl;
}
GeneralPurposeMemoryPool::~GeneralPurposeMemoryPool()
{
std::lock_guard<std::mutex> lock(_mutex); // 析构时也加锁
for (auto const& [blockSize, pool] : _pools)
{
delete pool;
}
_pools.clear();
std::cout << "GeneralPurposeMemoryPool destroyed." << std::endl;
}
GrowableFixedSizeMemoryPool* GeneralPurposeMemoryPool::findPool(std::size_t requestedSize)
{
// 需要加上 METADATA_SIZE 来考虑存储块大小的空间
std::size_t neededSize = requestedSize + METADATA_SIZE;
std::lock_guard<std::mutex> lock(_mutex);
// 寻找第一个能满足请求的池
for (auto const& [actualPoolBlockSize, pool] : _pools)
{
if (actualPoolBlockSize >= neededSize)
{
return pool;
}
}
return nullptr; // 没有找到合适的池
}
void* GeneralPurposeMemoryPool::allocate(std::size_t size)
{
if (size == 0) return nullptr;
GrowableFixedSizeMemoryPool* pool = findPool(size);
if (!pool)
{
std::cerr << "Error: No suitable pool found for size: " << size << std::endl;
throw std::bad_alloc();
}
// 从找到的池中分配原始内存块
char* rawMem = static_cast<char*>(pool->allocate());
if (!rawMem)
{
throw std::bad_alloc();
}
// 在用户数据区之前写入实际分配的块大小(包括元数据自身和用户数据)
// 这样在 deallocate 时才能知道该块是哪个池的。
std::size_t* metadataPtr = reinterpret_cast<std::size_t*>(rawMem);
*metadataPtr = pool->getBlockSize(); // 存储池的实际块大小
// 返回用户数据区的起始地址
return rawMem + METADATA_SIZE;
}
void GeneralPurposeMemoryPool::deallocate(void* ptr)
{
if (!ptr) return;
// 获取用户数据区之前的元数据
char* rawMem = static_cast<char*>(ptr) - METADATA_SIZE;
std::size_t* metadataPtr = reinterpret_cast<std::size_t*>(rawMem);
std::size_t actualBlockSize = *metadataPtr;
GrowableFixedSizeMemoryPool* pool = nullptr;
{
std::lock_guard<std::mutex> lock(_mutex);
auto it = _pools.find(actualBlockSize);
if (it != _pools.end())
{
pool = it->second;
}
}
if (!pool)
{
std::cerr << "Warning: Attempting to deallocate memory not owned by a known pool or metadata corrupted: " << ptr << std::endl;
// 尝试 fallback 到系统 delete
delete[] rawMem; // 或者直接返回,避免崩溃
return;
}
pool->deallocate(rawMem); // 将原始内存块归还给对应的池
}
// --- 通用内存池的示例用法 ---
class Foo {
int x;
char c[10];
public:
Foo(int val) : x(val) {
std::strncpy(c, "FooObj", sizeof(c) - 1);
c[sizeof(c)-1] = '';
// std::cout << "Foo(" << x << ") constructed." << std::endl;
}
~Foo() { /* std::cout << "Foo(" << x << ") destructed." << std::endl; */ }
void print() const { std::cout << " Foo: x=" << x << ", c=" << c << std::endl; }
};
class Bar {
double d;
long long l;
public:
Bar(double val, long long val2) : d(val), l(val2) {
// std::cout << "Bar(" << d << ") constructed." << std::endl;
}
~Bar() { /* std::cout << "Bar(" << d << ") destructed." << std::endl; */ }
void print() const { std::cout << " Bar: d=" << d << ", l=" << l << std::endl; }
};
int main_general_pool()
{
std::cout << "n--- GeneralPurposeMemoryPool Example ---" << std::endl;
// 定义我们希望支持的块大小。
// 内存池会根据这些大小创建内部的 FixedSizeMemoryPool。
// 实际分配的块会比这些大小更大,以容纳元数据和对齐。
std::vector<std::size_t> supportedBlockSizes = {
sizeof(Foo), // 14 bytes (on 64-bit)
sizeof(Bar), // 16 bytes (on 64-bit)
32, // 常用的小对象大小
64, // 常用的小对象大小
128 // 常用的小对象大小
};
GeneralPurposeMemoryPool generalPool(supportedBlockSizes, 5, 2); // 每个子池初始5块,每次增长2块
std::cout << "nAllocating objects from general pool:" << std::endl;
Foo* foo1 = nullptr;
Bar* bar1 = nullptr;
Foo* foo2 = nullptr;
char* customMem = nullptr;
try {
void* mem1 = generalPool.allocate(sizeof(Foo));
foo1 = new (mem1) Foo(100);
foo1->print();
void* mem2 = generalPool.allocate(sizeof(Bar));
bar1 = new (mem2) Bar(20.5, 300LL);
bar1->print();
void* mem3 = generalPool.allocate(sizeof(Foo));
foo2 = new (mem3) Foo(101);
foo2->print();
// 分配一个自定义大小的内存块
void* mem4 = generalPool.allocate(50); // 应该会选择 64 字节的池
customMem = new (mem4) char[50]; // 这是一个简单的char数组,没有构造函数
std::strncpy(customMem, "Custom memory block content", 49);
customMem[49] = '';
std::cout << " Custom memory: " << customMem << std::endl;
} catch (const std::bad_alloc& e) {
std::cerr << "Allocation failed: " << e.what() << std::endl;
}
std::cout << "nDeallocating objects:" << std::endl;
if (foo1) { foo1->~Foo(); generalPool.deallocate(foo1); }
if (bar1) { bar1->~Bar(); generalPool.deallocate(bar1); }
if (foo2) { foo2->~Foo(); generalPool.deallocate(foo2); }
if (customMem) { generalPool.deallocate(customMem); } // char数组没有析构函数,直接释放内存
std::cout << "n--- End GeneralPurposeMemoryPool Example ---" << std::endl;
return 0;
}
// 主函数调用
int main() {
main_general_pool(); // 运行通用内存池示例
// main(); // 运行固定大小内存池示例 (如果需要)
return 0;
}
通用内存池解析:
_pools成员:std::map<std::size_t, GrowableFixedSizeMemoryPool*>将实际的块大小映射到对应的GrowableFixedSizeMemoryPool实例。键值是GrowableFixedSizeMemoryPool实际管理的块大小(已包含元数据和对齐)。METADATA_SIZE: 这是一个关键常量,表示在每个分配的内存块前面存储元数据所需的字节数。这个元数据就是实际块的大小。- 构造函数: 遍历
supportedBlockSizes,为每个大小创建一个GrowableFixedSizeMemoryPool。在创建GrowableFixedSizeMemoryPool时,传入的blockSize需要加上METADATA_SIZE,以确保池分配的块足够容纳元数据。 findPool(std::size_t requestedSize): 根据用户请求的size,加上METADATA_SIZE得到实际需要的总大小。然后遍历_pools,找到第一个actualPoolBlockSize大于等于neededSize的池。这里利用了std::map键的有序性,或者简单遍历即可。allocate(std::size_t size):- 首先调用
findPool找到合适的池。 - 从该池中分配原始内存
rawMem。 - 在
rawMem的起始位置(即用户数据区之前)写入该块的实际大小pool->getBlockSize()。 - 返回
rawMem + METADATA_SIZE作为用户可以使用的内存地址。
- 首先调用
- *`deallocate(void ptr)`:**
- 通过
ptr - METADATA_SIZE得到原始内存块的起始地址rawMem。 - 从
rawMem的起始位置读取元数据,即actualBlockSize。 - 使用
actualBlockSize在_pools中查找对应的GrowableFixedSizeMemoryPool。 - 将
rawMem归还给找到的池。 - 如果找不到对应的池(可能元数据被破坏或
ptr不属于本池),则打印警告并可能尝试delete[]或直接返回。
- 通过
通用内存池的优缺点:
- 优点: 能够处理不同大小的内存请求,同时仍能利用固定大小池的确定性优势。对于预设的块大小,分配和释放速度非常快。
- 缺点:
- 内部碎片: 如果请求的大小与任何一个预设的块大小不完全匹配,会选择一个更大的块,导致内部碎片。
- 元数据开销: 每个分配的块都需要额外的空间来存储元数据。
- 池管理开销: 维护多个池的查找和管理逻辑增加了复杂性。
- 增长延迟: 如果某个子池需要增长,仍会引入系统调用延迟。
与 C++ new 和 delete 操作符集成
为了让自定义内存池更加透明和易用,我们可以重载 C++ 的 operator new 和 operator delete。这有两种主要方式:
- 全局重载: 重载全局的
operator new和operator delete。这会影响整个程序的所有new和delete调用。通常不推荐在库中使用,因为它可能与应用程序或其他库的自定义分配器冲突。但在一个完全由你控制的应用程序中,可以考虑。 - 类局部重载: 为特定的类重载
operator new和operator delete。这是更推荐的做法,因为它只影响该类的对象分配,不会干扰其他类型或全局分配。
类局部重载示例
// 假设 GeneralPurposeMemoryPool 已经定义并实例化为一个全局或单例
// 为了简化示例,我们将其作为一个全局变量
// 在实际项目中,它可能是一个单例或通过依赖注入的方式提供
GeneralPurposeMemoryPool g_generalPool({
sizeof(Foo), sizeof(Bar), 32, 64, 128
}, 10, 5); // 示例参数
class PooledObject {
public:
// 重载 operator new
static void* operator new(std::size_t size) {
std::cout << " Using custom new for PooledObject, size: " << size << std::endl;
return g_generalPool.allocate(size);
}
// 重载 operator delete
static void operator delete(void* ptr, std::size_t size) { // size参数是C++14及以后才有的
std::cout << " Using custom delete for PooledObject, size: " << size << std::endl;
g_generalPool.deallocate(ptr);
}
// 如果需要在 placement new 中使用,也可以重载 placement new
static void* operator new(std::size_t size, void* p) noexcept {
return p; // Placement new 只是返回传入的地址
}
static void operator delete(void* ptr, void* p) noexcept {
// placement delete 只有在 placement new 构造失败时才会被调用
// 通常不需要做任何事情,因为内存不是由 new 分配的
}
PooledObject() { /* std::cout << "PooledObject constructed." << std::endl; */ }
~PooledObject() { /* std::cout << "PooledObject destructed." << std::endl; */ }
virtual void doSomething() { std::cout << " PooledObject doing something." << std::endl; }
};
class DerivedPooledObject : public PooledObject {
int data;
public:
DerivedPooledObject(int d) : data(d) { /* std::cout << "DerivedPooledObject(" << data << ") constructed." << std::endl; */ }
~DerivedPooledObject() { /* std::cout << "DerivedPooledObject(" << data << ") destructed." << std::endl; */ }
void doSomething() override { std::cout << " DerivedPooledObject doing something, data: " << data << std::endl; }
};
int main_operator_overload() {
std::cout << "n--- Overloading operator new/delete Example ---" << std::endl;
PooledObject* po1 = new PooledObject(); // 会调用重载的 operator new
po1->doSomething();
delete po1; // 会调用重载的 operator delete
DerivedPooledObject* dpo1 = new DerivedPooledObject(42); // 同样会调用基类的 operator new/delete
dpo1->doSomething();
delete dpo1;
// 也可以通过 placement new 在池中显式分配
// 注意这里是 `new PooledObject()` 而不是 `new PooledObject`
// 如果没有为 PooledObject 重载 operator new(size_t, void*), 则会使用默认的 placement new.
// 我们已经重载了,所以这里会走我们重载的版本,但它只是返回传入的p
void* raw_mem = g_generalPool.allocate(sizeof(PooledObject));
PooledObject* po2 = new (raw_mem) PooledObject();
po2->doSomething();
po2->~PooledObject();
g_generalPool.deallocate(raw_mem); // 确保内存被返回到池中
std::cout << "n--- End Overloading operator new/delete Example ---" << std::endl;
return 0;
}
重载 operator new/delete 的注意事项:
- 签名匹配: 确保
operator new和operator delete的签名正确匹配。operator new(std::size_t)对应operator delete(void*)或operator delete(void*, std::size_t)。 - 异常安全:
operator new应该在分配失败时抛出std::bad_alloc。 placement new的重载:operator new(std::size_t, void*)只是返回传入的地址,它的主要作用是允许在已经分配好的内存上构造对象。对应的operator delete(void*, void*)只有在placement new构造函数抛出异常时才会被调用。- 基类与派生类: 如果基类重载了
operator new/delete,派生类会继承这些重载。如果派生类有自己的重载,则会使用派生类的。 - 数组
new[]/delete[]: 如果需要分配对象数组,还需要重载operator new[](std::size_t)和operator delete[](void*)。通常,数组分配的内存头部会包含数组元素数量的元数据,这使得数组的delete[]能够正确调用所有元素的析构函数。在我们的内存池中,由于是固定大小块,直接使用new (mem) Object[N]这种方式并不能很好地工作,因为mem只有一个地址。通常需要单独分配N个块,或者为数组设计一个专门的内存池。
实际考量与高级主题
1. 内存对齐的严格性
在我们的 FixedSizeMemoryPool 中,我们使用了 alignof(std::max_align_t) 来确保内存块满足最严格的对齐要求。这对于大多数数据类型是足够的。但如果应用程序使用了自定义的结构体或 SIMD 指令,可能需要更高的对齐(例如 16 字节、32 字节、64 字节)。在这种情况下,需要调整 AlignUp 函数中的 alignment 参数。
// 示例:要求 64 字节对齐
const std::size_t customAlignment = 64;
std::size_t actualBlockSize = AlignUp(requestedSize, customAlignment);
2. 伪共享(False Sharing)
在多线程环境中,如果两个线程频繁访问处于同一个缓存行(Cache Line)但属于不同对象的变量,即使它们逻辑上是独立的,CPU 也会因为缓存一致性协议而频繁地使缓存行失效,导致性能下降。这被称为伪共享。
内存池通过连续分配可能导致伪共享。例如,如果 FixedSizeMemoryPool 连续分配了两个小对象 objA 和 objB,而 objA 被线程1修改,objB 被线程2修改,它们可能共享同一个缓存行。
解决方案:
- 填充(Padding): 在分配的块之间添加额外的填充字节,确保不同线程访问的对象位于不同的缓存行。
- 线程局部池: 每个线程有自己独立的内存池,从根本上避免了不同线程在同一块内存区域争用。
3. 内存泄漏与调试
自定义内存池并不能自动防止内存泄漏。如果 allocate 了内存而没有调用 deallocate,内存仍然会泄漏。调试自定义内存池比调试系统堆更复杂,因为通用工具可能无法识别其内部结构。
调试技巧:
- 内存池统计: 记录已分配块的数量、空闲块的数量、总容量等。
- 魔术值(Magic Numbers): 在每个分配块的头部和尾部写入特定的模式(魔术值)。在
deallocate时检查这些值是否被覆盖,以检测越界写入。 - 已分配列表: 维护一个所有已分配块的列表(仅限调试版本),以便在程序结束时检查是否有未释放的块。
- 内存块状态: 在
FreeBlockHeader中添加一个bool is_allocated字段(仅限调试版本),并在allocate/deallocate时更新,用于检测双重释放或释放未分配内存。
4. 操作系统交互
我们示例中使用了 new char[] 来获取原始内存,这最终还是会通过 malloc 向操作系统请求。在极致的实时系统中,可能希望直接使用操作系统的内存映射功能,如 Linux 的 mmap 或 Windows 的 VirtualAlloc,来获取页对齐的、大块的、非交换(non-swappable)的内存。这样可以更好地控制内存,并避免 malloc 带来的额外开销和潜在问题。
// Linux mmap 示例 (简化)
#include <sys/mman.h>
#include <unistd.h>
void* allocate_pages(std::size_t numBytes) {
void* mem = mmap(nullptr, numBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (mem == MAP_FAILED) {
throw std::bad_alloc();
}
// 可以进一步使用 mlockall 或 mlock 来锁定内存,防止其被交换到磁盘
return mem;
}
void deallocate_pages(void* ptr, std::size_t numBytes) {
munmap(ptr, numBytes);
}
5. 区域分配器(Arena Allocators / Region-Based Allocation)
对于生命周期完全一致的一组对象,可以使用区域分配器。这种分配器一次性分配一个大块内存(一个“区域”),然后通过一个简单的指针递增来分配小块内存。当所有对象都不再需要时,只需一次性释放整个区域,而不需要单独释放每个对象。这种方式的释放速度极快(O(1)),且完全没有碎片,但要求所有对象的生命周期一致。
结语
自定义内存池是 C++ 在高性能和实时系统开发中的一项强大技术。它通过绕过传统动态内存分配机制的复杂性和不确定性,为我们提供了对内存管理更精细的控制,从而能够实现确定性的低延迟操作,并有效避免内存碎片和系统调用开销。从简单的固定大小池到复杂的通用池,选择哪种实现取决于你的具体需求和性能考量。理解其原理并合理运用,将是您在构建高性能 C++ 应用道路上的重要一步。当然,任何强大的工具都伴随着更高的复杂性,需要开发者投入更多精力进行设计、实现和调试。希望今天的讲座能为您打开这扇大门,助您在实时系统开发的征程中披荆斩棘。