1. 引言

内存管理在高性能系统中至关重要,特别是在需要频繁分配和释放大量小对象的场景。标准的new/deletemalloc/free操作会带来显著的性能开销。这就是为什么内存池技术变得尤为重要。

StdIndexedMemPool是一个高性能内存池实现,专为高并发环境设计,它提供了许多优势:

  • • 通过索引而非指针管理内存,减少内存占用

  • • 保证被回收的内存不会返回给操作系统,提高读取安全性

  • • 支持多线程并发访问,降低线程间竞争

  • • 提供智能指针接口,简化内存管理

    本教程将从基础概念开始,逐步深入介绍StdIndexedMemPool的工作原理和使用方法。


    2. 基础概念

    2.1 什么是内存池?

    内存池是一种内存分配策略,它预先分配一大块内存,然后将这块内存分成多个小块进行管理。当程序需要分配内存时,内存池会从预分配的内存中取出一块返回;当程序释放内存时,内存池会将这块内存回收供后续使用,而不是直接返回给操作系统。

    2.2 为什么需要内存池?

    • 1.性能提升:减少系统调用,避免频繁向操作系统申请和释放内存
    • 2.内存碎片减少:统一管理内存,减少内存碎片
    • 3.内存使用效率提高:可以更好地控制内存分配策略
    • 4. 降低内存泄漏风险:结合智能指针,简化内存管理

      2.3 StdIndexedMemPool的特点

      StdIndexedMemPool与普通内存池的主要区别在于:

      • 1.索引而非指针:返回整数索引而非直接指针,节省内存空间
      • 2.安全读取:即使在元素被回收后,仍然可以安全读取(需验证有效性)
      • 3.多线程优化:通过本地列表减少线程间竞争
      • 4.灵活的生命周期管理:支持不同的对象生命周期管理策略

        3. 内存池架构

        StdIndexedMemPool的核心架构由以下几个关键组件构成:

        3.1 CPU缓存本地性优化

        class CacheLocality {
        public:
            static size_t getNumCpus() {
                static const size_t numCpus = determineNumCpus();
                return numCpus;
            }
        };
        
        class AccessSpreader {
        public:
            static size_t current(size_t numStripes) {
                static thread_local unsigned currentCpu = threadId() % 256;
                return currentCpu % numStripes;
            }
        };

        这两个类负责处理CPU缓存本地性,通过将不同线程的内存访问分散到不同的内存条带,减少缓存争用。

        3.2 生命周期管理

        template <typename T, bool EagerRecycleWhenTrivial = true, bool EagerRecycleWhenNotTrivial = true>
        struct IndexedMemPoolTraits {
            static constexpr bool eagerRecycle() {
                return std::is_trivial<T>::value ? EagerRecycleWhenTrivial : EagerRecycleWhenNotTrivial;
            }
            
            static void initialize(T* ptr) { ... }
            static void cleanup(T* ptr) { ... }
            template <typename... Args> static void onAllocate(T* ptr, Args&&... args) { ... }
            static void onRecycle(T* ptr) { ... }
        };

        Traits类定义了对象的生命周期管理方式,支持两种主要策略:

        • • 急切回收:对象在分配时构造,回收时析构

        • • 惰性回收:对象在首次分配时构造,只在池销毁时析构

        3.3 内存结构

        内存池的核心数据结构:

        struct Slot {
            alignas(T) char elemStorage[sizeof(T)];  // 元素存储空间
            Atom<uint32_t> localNext;                // 本地链表的下一个索引
            Atom<uint32_t> globalNext;               // 全局链表的下一个索引
        };
        
        struct TaggedPtr {
            uint32_t idx;           // 索引
            uint32_t tagAndSize;    // 标签和大小
        };
        
        struct LocalList {
            Atom<TaggedPtr> head;   // 列表头
        };
        • • Slot:存储实际元素和链接信息

        • • TaggedPtr:用于原子操作的带标签指针

        • • LocalList:线程本地列表

        3.4 内存分配和回收

        内存池使用三级结构进行内存管理:

        • 1.线程本地列表:每个线程有自己的本地列表,减少线程间竞争
        • 2.全局空闲列表:当本地列表为空或满时,与全局列表交互
        • 3.未分配内存:当全局列表为空时,分配新内存

        4. 使用方法

        下面通过一个简单的例子来展示如何使用StdIndexedMemPool:

        #include "StdIndexedMemPool.h"
        #include <iostream>
        
        struct MyObject {
            int id;
            std::string name;
            
            MyObject() : id(0), name("未初始化") {}
            MyObject(int i, std::string n) : id(i), name(std::move(n)) {}
            
            void print() const {
                std::cout << "对象ID: " << id << ", 名称: " << name << std::endl;
            }
        };
        
        int main() {
            // 创建一个可容纳1000个MyObject的内存池
            std_mem_pool::IndexedMemPool<MyObject> pool(1000);
            
            // 使用索引分配和访问
            uint32_t idx1 = pool.allocIndex(1, "对象1");
            if (idx1 != 0) {
                pool[idx1].print();  // 使用operator[]访问
            }
            
            // 使用智能指针
            auto ptr2 = pool.allocElem(2, "对象2");
            if (ptr2) {
                ptr2->print();  // 智能指针会自动回收
            }
            
            // 手动回收索引
            pool.recycleIndex(idx1);
            
            // 重新分配
            uint32_t idx3 = pool.allocIndex(3, "对象3");
            if (idx3 != 0) {
                pool[idx3].print();
            }
            
            return 0;
        }

        4.1 创建内存池

        // 创建一个可容纳1000个MyObject的内存池
        std_mem_pool::IndexedMemPool<MyObject> pool(1000);

        内存池构造函数接受一个参数:容量。这表示即使所有本地列表都满了,内存池仍然可以分配的元素数量。

        4.2 分配内存

        StdIndexedMemPool提供两种分配方式:

        // 分配并返回索引
        uint32_t idx = pool.allocIndex(1, "对象1");
        
        // 分配并返回智能指针
        auto ptr = pool.allocElem(2, "对象2");
        • • allocIndex返回一个整数索引,需要手动回收

        • • allocElem返回一个智能指针,会自动回收

          4.3 访问元素

          // 通过索引访问
          pool[idx].print();
          
          // 通过智能指针访问
          ptr->print();

            4.4 回收内存

            // 手动回收索引
            pool.recycleIndex(idx);
            
            // 智能指针会在离开作用域时自动回收
            {
                auto ptr = pool.allocElem(4, "临时对象");
                // 使用ptr...
            } // ptr离开作用域,自动回收

            5. 高级特性

            5.1 生命周期管理

            StdIndexedMemPool支持自定义对象的生命周期管理:

            // 使用急切回收策略
            std_mem_pool::IndexedMemPool<MyObject, 32, 200, std::atomic, 
                std_mem_pool::IndexedMemPoolTraitsEagerRecycle<MyObject>> pool1(1000);
            
            // 使用惰性回收策略
            std_mem_pool::IndexedMemPool<MyObject, 32, 200, std::atomic, 
                std_mem_pool::IndexedMemPoolTraitsLazyRecycle<MyObject>> pool2(1000);
            • • 急切回收:适合状态频繁变化的对象,确保每次分配都得到全新状态

            • • 惰性回收:适合初始化开销大但状态变化少的对象

            5.2 线程安全性

            StdIndexedMemPool设计为线程安全的,可以在多线程环境中使用:

            #include <thread>
            #include <vector>
            
            void worker(std_mem_pool::IndexedMemPool<MyObject>& pool, int id) {
                for (int i = 0; i < 100; i++) {
                    auto ptr = pool.allocElem(id * 1000 + i, "线程对象");
                    // 使用对象...
                    // 离开作用域时自动回收
                }
            }
            
            int main() {
                std_mem_pool::IndexedMemPool<MyObject> pool(10000);
                
                std::vector<std::thread> threads;
                for (int i = 0; i < 10; i++) {
                    threads.emplace_back(worker, std::ref(pool), i);
                }
                
                for (auto& t : threads) {
                    t.join();
                }
                
                return 0;
            }

            内存池通过本地列表机制降低线程间的竞争,使其在高并发环境中表现优异。

            5.3 内存安全性

            即使元素被回收,StdIndexedMemPool也允许安全读取它们(但需要验证有效性):

            uint32_t idx = pool.allocIndex(5, "测试对象");
            pool[idx].print();  // 正常访问
            
            pool.recycleIndex(idx);
            
            // 回收后仍然可以读取,但需要验证
            if (pool.isAllocated(idx)) {
                pool[idx].print();  // 不会执行到这里
            } else {
                std::cout << "索引 " << idx << " 已被回收" << std::endl;
            }

            isAllocated方法可以检查索引是否仍然有效。


            6.1 内存分配策略

            StdIndexedMemPool使用mmap预分配整个地址空间,但延迟元素构造:

            slots_ = static_cast<Slot*>(mmap(
                nullptr,
                mmapLength_,
                PROT_READ | PROT_WRITE,
                MAP_PRIVATE | MAP_ANONYMOUS,
                -1,
                0));

            这种方式有几个优点:

            • • 地址空间连续,便于索引计算

            • • 按需实际分配物理内存,减少内存占用

            • • 避免频繁的系统调用

              6.2 多级列表结构

              内存池使用多级列表结构:

              • 1. 本地列表:每个线程有自己的本地列表,减少争用
              • 2. 全局列表:当本地列表为空或满时,与全局列表交互
                uint32_t localPop(Atom<TaggedPtr>& head) {
                    // 先尝试从本地列表获取
                    if (h.idx != 0) {
                        // 本地列表非空,尝试弹出
                        // ...
                    }
                
                    // 如果本地列表为空,尝试从全局列表获取
                    uint32_t idx = globalPop();
                    if (idx == 0) {
                        // 如果全局列表也为空,分配新内存
                        // ...
                    }
                    // ...
                }

                6.3 原子操作和无锁算法

                StdIndexedMemPool大量使用原子操作和无锁算法,避免传统锁带来的性能损失:

                void localPush(Atom<TaggedPtr>& head, uint32_t idx) {
                    Slot& s = slot(idx);
                    TaggedPtr h = head.load(std::memory_order_acquire);
                    bool recycled = false;
                    while (true) {
                        s.localNext.store(h.idx, std::memory_order_release);
                        if (!recycled) {
                            Traits::onRecycle(slot(idx).elemPtr());
                            recycled = true;
                        }
                
                        if (h.size() == LocalListLimit) {
                            // 推入将溢出本地列表,改为窃取它
                            if (head.compare_exchange_strong(h, h.withEmpty())) {
                                // 窃取成功,将所有内容放入全局列表
                                globalPush(s, idx);
                                return;
                            }
                        } else {
                            // 本地列表有空间
                            if (head.compare_exchange_strong(h, h.withIdx(idx).withSizeIncr())) {
                                // 成功
                                return;
                            }
                        }
                        // h被失败的CAS更新
                    }
                }

              使用compare_exchange_strong等原子操作实现无锁算法,大幅提升并发性能。

              6.4 TaggedPtr防止ABA问题

              为防止ABA问题(一个值从A变成B又变回A),StdIndexedMemPool使用带标签的指针:

              struct TaggedPtr {
                  uint32_t idx;
                  uint32_t tagAndSize;
                  
                  // 每次操作都会增加标签值
                  TaggedPtr withIdx(uint32_t repl) const {
                      return TaggedPtr{repl, tagAndSize + TagIncr};
                  }
              };

              每次修改指针都会增加标签值,即使指针值相同,标签值也会不同,有效防止ABA问题。

              7. 常见问题和最佳实践

              7.1 内存池容量选择

              内存池容量应该根据实际需求来选择:

              • • 太小:可能导致分配失败

              • • 太大:浪费内存

                建议根据预期峰值用量的1.2-1.5倍来设置容量。

                7.2 回收策略选择

                根据对象特性选择合适的回收策略:

                • • 急切回收:适合状态频繁变化的对象

                • • 惰性回收:适合初始化开销大但状态变化少的对象

                  7.3 避免内存泄漏

                  尽管StdIndexedMemPool简化了内存管理,但仍需注意:

                  • • 优先使用allocElem返回的智能指针

                  • • 如果使用allocIndex,确保配对调用recycleIndex

                  • • 验证索引有效性,避免使用已回收的索引

                    7.4 性能优化

                    • • 合理设置本地列表数量(NumLocalLists)和大小限制(LocalListLimit)

                    • • 避免频繁创建和销毁内存池

                    • • 对于短生命周期的对象,考虑使用线程本地内存池

                    8. 完整示例

                    下面是一个完整的示例,展示了StdIndexedMemPool的主要功能:

                    #include "StdIndexedMemPool.h"
                    #include <iostream>
                    #include <string>
                    #include <thread>
                    #include <vector>
                    
                    // 自定义对象
                    struct User {
                        int id;
                        std::string name;
                        std::vector<int> data;
                        
                        User() : id(0), name("未初始化") {}
                        User(int i, std::string n) : id(i), name(std::move(n)) {
                            // 模拟一些数据
                            data.resize(10, i);
                        }
                        
                        void print() const {
                            std::cout << "用户ID: " << id << ", 名称: " << name 
                                      << ", 数据大小: " << data.size() << std::endl;
                        }
                    };
                    
                    // 测试函数
                    void testBasicUsage() {
                        std::cout << "=== 基本用法测试 ===" << std::endl;
                        
                        // 创建内存池
                        std_mem_pool::IndexedMemPool<User> pool(100);
                        
                        // 分配索引
                        uint32_t idx1 = pool.allocIndex(1, "用户1");
                        if (idx1 != 0) {
                            std::cout << "分配索引: " << idx1 << std::endl;
                            pool[idx1].print();
                        }
                        
                        // 分配智能指针
                        auto ptr2 = pool.allocElem(2, "用户2");
                        if (ptr2) {
                            std::cout << "分配智能指针, 用户ID: " << ptr2->id << std::endl;
                            ptr2->print();
                        }
                        
                        // 手动回收
                        std::cout << "回收索引: " << idx1 << std::endl;
                        pool.recycleIndex(idx1);
                        
                        // 验证回收
                        std::cout << "索引 " << idx1 << " 是否已分配: " 
                                  << (pool.isAllocated(idx1) ? "是" : "否") << std::endl;
                        
                        // 重新分配
                        uint32_t idx3 = pool.allocIndex(3, "用户3");
                        std::cout << "重新分配索引: " << idx3 << std::endl;
                        pool[idx3].print();
                    }
                    
                    // 多线程测试
                    void testMultithreading() {
                        std::cout << "\n=== 多线程测试 ===" << std::endl;
                        
                        std_mem_pool::IndexedMemPool<User> pool(1000);
                        
                        auto worker = [&pool](int threadId) {
                            for (int i = 0; i < 10; i++) {
                                auto ptr = pool.allocElem(threadId * 100 + i, 
                                                         "线程" + std::to_string(threadId) + 
                                                         "用户" + std::to_string(i));
                                if (ptr) {
                                    // 模拟一些操作
                                    ptr->data.push_back(i);
                                }
                                // 智能指针自动回收
                            }
                        };
                        
                        std::vector<std::thread> threads;
                        for (int i = 0; i < 5; i++) {
                            threads.emplace_back(worker, i);
                        }
                        
                        for (auto& t : threads) {
                            t.join();
                        }
                        
                        std::cout << "多线程测试完成, 最大分配索引: " << pool.maxAllocatedIndex() << std::endl;
                    }
                    
                    int main() {
                        testBasicUsage();
                        testMultithreading();
                        return 0;
                    }

                    9. 总结

                    StdIndexedMemPool是一个功能强大的高性能内存池实现,它通过索引而非指针管理内存,支持多线程并发访问,并提供灵活的对象生命周期管理。

                    主要优势:

                    • • 使用索引而非指针,节省内存

                    • • 多级列表结构和无锁算法,提高并发性能

                    • • 灵活的生命周期管理,适应不同场景

                    • • 智能指针接口,简化内存管理

                      在需要频繁分配和释放大量小对象的高性能系统中,StdIndexedMemPool是一个理想的选择。

                      源代码:链接: https://pan.baidu.com/s/1m9q-ZvRvvUEHV-cDTCooXQ 提取码: tojl

                      希望本教程能帮助你理解并开始使用这个强大的内存池库。如有任何问题,欢迎进一步交流!

                        Logo

                        openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

                        更多推荐