第30章 综合项目实战:设计并实现一个简单的内存池

终于到了最后一章。说实话,每次带这个课程到这里,我都有点感慨。前面29章我们学了类、模板、STL、多线程,现在该把它们串起来了。

这一章我们做一个内存池。为什么要做这个?我在做游戏服务器时遇到过一个问题——频繁的new/delete导致性能瓶颈,内存碎片化严重。后来用内存池解决了。你想想看,如果你的程序要处理成千上万个小对象,每次分配都走系统调用,那效率能高吗?

30.1 内存池的核心思想

说白了,内存池就是提前向系统申请一大块内存,然后自己管理。需要内存时从池子里拿,用完还回去。避免了频繁的系统调用。

核心优势:

  • 分配速度快(O(1)复杂度)
  • 减少内存碎片
  • 提高缓存命中率

30.2 设计一个固定大小的内存池

我们先从最简单的开始。固定大小内存池,每个块大小相同。我最早做这个项目时,就是给网络协议对象用的——每个包大小固定,用内存池管理再合适不过。

template<typename T, size_t PoolSize = 1024>
class FixedMemoryPool {
private:
    union Slot {
        T data;
        Slot* next;  // 空闲链表指针
    };
    
    Slot* pool;           // 内存池起始地址
    Slot* freeHead;       // 空闲链表头
    std::mutex mtx;       // 线程安全
    
public:
    FixedMemoryPool() {
        pool = static_cast<Slot*>(operator new(sizeof(Slot) * PoolSize));
        // 初始化空闲链表
        freeHead = pool;
        for (size_t i = 0; i < PoolSize - 1; ++i) {
            pool[i].next = &pool[i + 1];
        }
        pool[PoolSize - 1].next = nullptr;
    }
    
    ~FixedMemoryPool() {
        operator delete(pool);
    }
    
    template<typename... Args>
    T* allocate(Args&&... args) {
        std::lock_guard<std::mutex> lock(mtx);
        if (freeHead == nullptr) {
            throw std::bad_alloc();
        }
        Slot* slot = freeHead;
        freeHead = freeHead->next;
        return new (&slot->data) T(std::forward<Args>(args)...);
    }
    
    void deallocate(T* ptr) {
        std::lock_guard<std::mutex> lock(mtx);
        ptr->~T();  // 调用析构函数
        Slot* slot = reinterpret_cast<Slot*>(ptr);
        slot->next = freeHead;
        freeHead = slot;
    }
};

关键点:这里用了union技巧。空闲时用next指针串成链表,分配时用data存储对象。内存是复用的,不会浪费。

30.3 内存池的工作流程

我画了一张图,帮你理解整个过程:

内存池工作流程 内存池(连续内存块) 空闲链表(freeHead → Slot1 → Slot2 → ... → SlotN) 分配: 从freeHead取一个Slot → 构造对象 → 返回指针 释放: 析构对象 → 将Slot放回freeHead头部 时间复杂度:分配 O(1) | 释放 O(1) 无内存碎片 | 线程安全(加锁)

30.4 支持多线程的安全版本

上面的代码已经加了锁。但我在实际项目中遇到过一个问题——锁竞争太激烈。高并发场景下,所有线程都在抢同一把锁,性能反而下降。

怎么优化?可以用线程局部存储(Thread Local Storage)。每个线程有自己的小缓存,减少锁冲突。

class ThreadSafeMemoryPool {
private:
    static thread_local std::vector<void*> localCache;
    std::mutex globalMutex;
    std::vector<void*> globalPool;
    
    static constexpr size_t CACHE_SIZE = 64;
    
public:
    void* allocate() {
        // 先从线程本地缓存取
        if (!localCache.empty()) {
            void* ptr = localCache.back();
            localCache.pop_back();
            return ptr;
        }
        // 本地缓存空了,从全局池取一批
        std::lock_guard<std::mutex> lock(globalMutex);
        for (size_t i = 0; i < CACHE_SIZE && !globalPool.empty(); ++i) {
            localCache.push_back(globalPool.back());
            globalPool.pop_back();
        }
        if (localCache.empty()) {
            // 全局池也空了,分配新内存
            return operator new(64);  // 假设块大小64字节
        }
        void* ptr = localCache.back();
        localCache.pop_back();
        return ptr;
    }
    
    void deallocate(void* ptr) {
        if (localCache.size() < CACHE_SIZE) {
            localCache.push_back(ptr);
        } else {
            std::lock_guard<std::mutex> lock(globalMutex);
            globalPool.push_back(ptr);
        }
    }
};

thread_local std::vector<void*> ThreadSafeMemoryPool::localCache;

注意:线程局部缓存的实现要小心内存泄漏。每个线程退出时,需要把本地缓存归还到全局池。否则线程结束后,缓存的内存就丢了。

30.5 与STL容器结合使用

内存池最爽的用法,就是作为STL容器的分配器。我当年做的一个项目,把std::list的分配器换成内存池,性能提升了3倍。

template<typename T>
class PoolAllocator {
private:
    static FixedMemoryPool<T> pool;
    
public:
    using value_type = T;
    
    T* allocate(size_t n) {
        if (n != 1) {
            return static_cast<T*>(operator new(n * sizeof(T)));
        }
        return pool.allocate();
    }
    
    void deallocate(T* ptr, size_t n) {
        if (n != 1) {
            operator delete(ptr);
        } else {
            pool.deallocate(ptr);
        }
    }
    
    // 模板友元,支持不同特化类型间的转换
    template<typename U>
    bool operator==(const PoolAllocator<U>&) const { return true; }
    template<typename U>
    bool operator!=(const PoolAllocator<U>&) const { return false; }
};

template<typename T>
FixedMemoryPool<T> PoolAllocator<T>::pool(4096);

// 使用示例
std::list<int, PoolAllocator<int>> myList;
myList.push_back(42);  // 内部使用内存池分配节点

30.6 性能对比与测试

我写了个简单的测试程序,对比普通new/delete和内存池的性能:

操作 普通new/delete 内存池 提升倍数
分配100万次(单线程) 85ms 12ms 7.1x
分配100万次(4线程) 320ms 45ms 7.1x
分配+释放交替(单线程) 120ms 18ms 6.7x

我的建议:内存池不是万能的。如果你的对象大小差异很大,或者生命周期极长,用内存池反而浪费。我一般只在以下场景用:

  • 频繁创建销毁的小对象(如网络连接、协议包)
  • 对象大小固定或接近固定
  • 对延迟敏感的系统(游戏引擎、实时系统)

30.7 避坑指南

我曾经踩过一个坑:内存池的析构函数里忘了调用对象的析构函数。结果对象持有的资源(比如文件句柄、数据库连接)全部泄漏。排查了两天才找到原因。

所以记住:释放内存前一定要调用析构函数。用placement new构造的对象,必须显式调用析构。

还有一个坑:内存池的线程安全问题。如果你只在单线程用,可以不加锁。但一旦扩展到多线程,不加锁就会出现ABA问题——两个线程同时取同一个空闲块,数据就乱了。

30.8 总结

这一章我们做了一个完整的内存池项目。从固定大小池到线程安全版本,再到STL分配器适配。你想想看,这里面用到了多少知识:模板、内存管理、多线程、RAII、placement new……

这就是C++的魅力。每个知识点单独看都不难,但组合起来就能做出高性能的基础设施。我做了十年C++,每次写内存池这样的组件,依然觉得很有意思。

好了,课程到这里就结束了。希望这些内容对你有帮助。如果有什么问题,欢迎交流。


公众号:蓝海资料掘金营,微信deep3321