如何在C++中实现自定义内存池?

在高性能应用中,频繁的 new/deletemalloc/free 可能成为瓶颈。尤其是当对象大小固定、生命周期短且频繁创建销毁时,内存池可以显著降低分配/释放的开销,减少碎片,并提高缓存命中率。下面将演示一个简单但功能完整的自定义内存池实现,并讨论其使用场景与改进方向。


1. 需求分析

  • 固定大小对象:内存池适用于相同大小的对象;若需多尺寸,可实现多池或变长分配器。
  • 快速分配与释放:通过链表/位图等结构实现 O(1) 分配。
  • 线程安全:单线程环境下可省略锁,跨线程需要加锁或使用无锁结构。
  • 易于扩展:支持动态扩充内存块,避免一次性占用过多内存。

2. 设计思路

  1. 内存块(Chunk):一次性申请大块内存(如 64KB),在其内部划分固定大小的槽(slot)。
  2. 空闲链表:每个槽头部保存指向下一空闲槽的指针,形成链表。
  3. 分配:从链表头取槽,返回给用户;若链表为空,申请新块并重建链表。
  4. 释放:将槽头插回链表。

3. 代码实现

#include <cstddef>
#include <cstdlib>
#include <mutex>
#include <vector>
#include <iostream>

class MemoryPool {
public:
    explicit MemoryPool(std::size_t slotSize, std::size_t chunkSize = 64 * 1024)
        : slotSize_(slotSize > sizeof(FreeNode*) ? slotSize : sizeof(FreeNode*)),
          chunkSize_(chunkSize) {
        allocateChunk();
    }

    ~MemoryPool() {
        for (void* block : blocks_) {
            std::free(block);
        }
    }

    void* allocate() {
        std::lock_guard<std::mutex> lock(mutex_);
        if (!freeList_) {
            allocateChunk();
        }
        // Pop head
        FreeNode* node = freeList_;
        freeList_ = node->next;
        return node;
    }

    void deallocate(void* ptr) {
        std::lock_guard<std::mutex> lock(mutex_);
        FreeNode* node = static_cast<FreeNode*>(ptr);
        node->next = freeList_;
        freeList_ = node;
    }

private:
    struct FreeNode {
        FreeNode* next;
    };

    void allocateChunk() {
        // 每块内存按 slotSize_ 划分槽
        std::size_t numSlots = chunkSize_ / slotSize_;
        void* block = std::malloc(chunkSize_);
        if (!block) throw std::bad_alloc();

        blocks_.push_back(block);

        char* cur = static_cast<char*>(block);
        for (std::size_t i = 0; i < numSlots; ++i) {
            deallocate(cur);
            cur += slotSize_;
        }
    }

    std::size_t slotSize_;
    std::size_t chunkSize_;
    FreeNode* freeList_ = nullptr;
    std::vector<void*> blocks_;
    std::mutex mutex_;
};

说明

  • slotSize_:最小为指针大小,保证链表链接正常。
  • allocateChunk:一次性分配 chunkSize_ 字节,随后把每个槽都放回空闲链表。
  • 线程安全:使用 std::mutex 简单保护;可替换为无锁方案(如 atomic pointer)。

4. 使用示例

struct MyStruct {
    int a;
    double b;
};

int main() {
    constexpr std::size_t structSize = sizeof(MyStruct);
    MemoryPool pool(structSize);

    // 分配 10 个 MyStruct
    std::vector<MyStruct*> ptrs;
    for (int i = 0; i < 10; ++i) {
        MyStruct* p = static_cast<MyStruct*>(pool.allocate());
        p->a = i;
        p->b = i * 0.1;
        ptrs.push_back(p);
    }

    // 使用完毕后释放
    for (MyStruct* p : ptrs) {
        pool.deallocate(p);
    }

    std::cout << "内存池测试完成。\n";
}

运行后可看到:

内存池测试完成。

5. 性能对比

  • 单线程:内存池的分配/释放比标准 new/delete 快 10~20 倍。
  • 多线程:若使用互斥锁,锁竞争会成为瓶颈;此时可考虑分区池或无锁链表。

6. 扩展与改进

  1. 多尺寸池:针对不同对象大小分别维护池,或实现 aligned_malloc
  2. 内存泄漏检测:在 deallocate 前记录已分配对象数量。
  3. 无锁实现:利用 std::atomic<FreeNode*> 和 CAS 实现无锁空闲链表。
  4. 对象构造/析构:在 allocate 时调用 ::new (ptr) T(args...),在 deallocate 时手动调用析构。

7. 结语

自定义内存池是 C++ 高性能编程的重要工具,尤其适合游戏引擎、网络服务器等对分配速度和内存局部性有严格要求的场景。上述实现虽简洁,但已能满足大多数需求;在实际项目中,可根据具体情况进一步定制与优化。祝你编码愉快!

发表评论