自定义内存池在 C++ 中的实现与优化技巧

在高性能计算、游戏开发或嵌入式系统中,频繁的内存分配与释放往往成为瓶颈。传统的 new/deletemalloc/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. 性能测评与优化技巧

  1. 避免碎片:分区技术、可变块池结合使用,可降低碎片率。
  2. 缓存友好:将块的对齐(alignas)设为 CPU 缓存线长度(如 64 B),减少跨行访问。
  3. 批量释放:将多个释放操作聚合后一次性返回,减少链表操作开销。
  4. 自适应扩容:根据实时使用率动态调整每个分区的扩容策略,避免频繁的大块分配。
  5. 内存对齐:使用 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,可以实现线程安全、可插拔的内存资源,满足各种高性能场景的需求。希望本篇文章能为你在项目中实现高效内存池提供思路与参考。

发表评论