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

在高性能系统中,频繁的new/delete往往会导致大量的碎片化和内存碎片,进而影响缓存命中率、产生不必要的系统调用。为了解决这一问题,开发者常常采用自定义内存池(Memory Pool)来统一管理一块连续的内存区域,并在此区域内部按需分配和回收内存。本文将介绍一种简单且可扩展的内存池实现方式,并讨论其在多线程环境中的应用和优化思路。

1. 设计目标

  1. 高效分配:分配和回收操作的时间复杂度尽量为 O(1)。
  2. 内存对齐:支持用户指定的对齐方式,满足结构体对齐需求。
  3. 可扩展性:当池空间不足时,能够自动扩容。
  4. 线程安全:多线程环境下能够安全使用。

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_;
};

关键点说明

  1. 对齐:使用 alignUp 将块大小向上取整到对齐值,确保每块地址满足对齐要求。
  2. 扩容:在池满时,使用 realloc 扩大内存区,然后把新增的块链接进空闲链表。扩容频率可以通过策略调整,例如仅在块数达到一定阈值后才扩容。
  3. 线程安全:使用 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++ 程序在高并发、低延迟场景下的性能。通过固定块大小、链表管理以及必要的线程安全措施,我们可以得到一个简洁而高效的实现。根据业务场景进一步扩展功能,如分区池、无锁实现等,可使内存池更加适配复杂系统需求。

发表评论