【问题标题】:C++11 memory pool design pattern?C++11内存池设计模式?
【发布时间】:2013-04-29 00:02:48
【问题描述】:

我有一个程序包含一个处理阶段,该阶段需要使用来自多态类型树的一堆不同对象实例(全部在堆上分配),所有这些实例最终都派生自一个公共基类。

由于实例可能会周期性地相互引用,并且没有明确的所有者,我想用new 分配它们,用原始指针处理它们,并将它们留在内存中以供阶段使用(即使它们变得未引用) ,然后在使用这些实例的程序阶段之后,我想一次将它们全部删除。

我的构想如下:

struct B; // common base class

vector<unique_ptr<B>> memory_pool;

struct B
{
    B() { memory_pool.emplace_back(this); }

    virtual ~B() {}
};

struct D : B { ... }

int main()
{
    ...

    // phase begins
    D* p = new D(...);

    ...

    // phase ends
    memory_pool.clear();
    // all B instances are deleted, and pointers invalidated

    ...
}

除了注意所有的B实例都用new分配,并且在内存池被清除后没有人使用指向它们的任何指针之外,这个实现是否有问题?

我特别担心this 指针用于在派生类构造函数完成之前在基类构造函数中构造std::unique_ptr。这会导致未定义的行为吗?如果有,有什么解决方法吗?

【问题讨论】:

  • 为什么B 需要知道内存池?从驱动程序代码 (main) 添加指针看起来是不可估量的可取之处。然后当然可以限制池的生命周期,这反过来意味着当它超出范围时您可以免费销毁。
  • @Jon:您的意思是用memory_pool.emplace_back(new D(...)) 替换对new D(...) 的调用?我想我可以编写一个make_shared 风格的模板函数来构造对象,将参数转发给构造函数,并将其添加到内存池中。我还是想知道上面的代码是不是UB。
  • 对于知名的 FOSS 解决方案,请查看The Boehm-Demers-Weiser conservative C/C++ Garbage Collector out。

标签: c++ c++11 memory-pool


【解决方案1】:

如果您还没有,请熟悉Boost.Pool。来自 Boost 文档:

什么是池?

池分配是一种非常快的内存分配方案,但是 限制了它的使用。有关池分配的更多信息(也 称为简单隔离存储,参见concepts 概念和Simple Segregated Storage

为什么要使用 Pool?

使用池可以让您更好地控制内存的使用方式 程序。例如,您可能有一种情况,您想 在一个点分配一堆小对象,然后到达一个点 在您的程序中不再需要它们。使用池 接口,您可以选择运行它们的析构函数或直接删除它们 被遗忘;池接口将保证没有 系统内存泄漏。

什么时候应该使用 Pool?

池通常在有大量分配和 小对象的释放。另一个常见的用法是情况 上面,很多对象可能会从内存中删除。

一般来说,当您需要一种更有效的方式来处理不寻常的事情时,请使用池 内存控制。

我应该使用哪个池分配器?

pool_allocator 是一个更通用的解决方案,面向 有效地为任意数量的连续块的请求提供服务。

fast_pool_allocator 也是一种通用解决方案,但适合 一次有效地服务一个块的请求;它 将适用于连续的块,但不如pool_allocator

如果您非常关心性能,请使用 fast_pool_allocator 在处理std::list 等容器时, 并在处理诸如容器之类的容器时使用pool_allocator std::vector.

内存管理是一项棘手的工作(线程、缓存、对齐、碎片等)。对于严肃的生产代码,精心设计和精心优化的库是必经之路,除非您的分析器证明存在瓶颈。

【讨论】:

    【解决方案2】:

    我仍然认为这是一个没有明确答复的有趣问题,但请让我将其分解为您实际提出的不同问题:

    1.) 在初始化子类之前将指向基类的指针插入向量是否会防止或导致从该指针检索继承类的问题。 [例如切片。]

    回答:不,只要您 100% 确定所指向的相关类型,此机制不会导致这些问题,但请注意以下几点:

    如果派生构造函数失败,那么当你可能有一个悬空指针至少位于向量中时,你会遇到一个问题,因为它[派生类]认为它正在获得的地址空间将被释放到运行环境失败,但向量的地址仍然是基类类型。

    请注意,矢量虽然有点用,但并不是最好的结构,即使是这样,这里也应该涉及一些控制反转,以允许矢量对象控制对象的初始化,以便你有成功/失败的意识。

    这些点引出了隐含的第二个问题:

    2.) 这是一个很好的池化模式吗?

    答案:不是真的,由于上述原因,加上其他原因(将向量推过它的端点基本上会以 malloc 结束,这是不必要的,并且会影响性​​能。)理想情况下,您希望使用池化库,或者模板类,甚至更好的是,将分配/取消分配策略实现与池实现分开,已经暗示了一个低级解决方案,即从池初始化中分配足够的池内存,然后使用指针来使用它void 从池地址空间内(请参阅上面的 Alex Zywicki 的解决方案。)使用这种模式,池销毁是安全的,因为池将是连续内存,可以在没有任何悬空问题的情况下整体销毁,或者由于丢失所有引用而导致内存泄漏一个对象(丢失对其地址由存储管理器通过池分配的对象的所有引用会留下脏块,但不会导致内存泄漏,因为它由池 i 管理实现。

    在 C/C++ 的早期(在 STL 大规模扩散之前),这是一个很好的讨论模式,许多实现和设计都可以在很好的文献中找到:例如:

    Knuth(1973 年计算机编程的艺术:多卷),有关更完整的列表以及关于池的更多信息,请参阅:

    http://www.ibm.com/developerworks/library/l-memory/

    第三个隐含的问题似乎是:

    3) 这是使用池的有效场景吗?

    Answer:这是一个本地化的设计决策,基于您对什么感到满意,但老实说,您的实现(没有控制结构/聚合,可能循环共享对象子集)向我表明您会更好使用包装对象的基本链接列表,每个对象都包含一个指向您的超类的指针,仅用于寻址目的。您的循环结构是建立在此之上的,您只需根据需要修改/扩大缩小列表,以根据需要容纳所有第一类对象,完成后,您可以轻松地在 O(1) 操作中有效地销毁它们从链表中。

    话虽如此,我个人还是建议此时(当您遇到池化确实有用的场景,因此您的心态正确时)进行存储管理/池化集的构建现在参数化/无类型的类的数量,因为它将为您提供良好的未来。

    【讨论】:

      【解决方案3】:

      这听起来像是我听说的线性分配器。 我将解释我如何理解它的基本原理。

      1. 使用 ::operator new(size) 分配一块内存;
      2. 有一个 void* 是指向内存中下一个可用空间的指针。
      3. 您将拥有一个 alloc(size_t size) 函数,该函数将为您提供一个指向块中从第一步开始的位置的指针,供您构建以使用 Placement New
      4. 放置新看起来像... int* i = new(location)int();其中 location 是您从分配器分配的内存块的 void*。
      5. 用完所有内存后,您将调用 Flush() 函数,该函数将从池中释放内存或至少清除数据。

      我最近编写了其中一个,我将在这里为您发布我的代码并尽我所能解释。

          #include <iostream>
          class LinearAllocator:public ObjectBase
          {
          public:
              LinearAllocator();
              LinearAllocator(Pool* pool,size_t size);
              ~LinearAllocator();
              void* Alloc(Size_t size);
              void Flush();
          private:
              void** m_pBlock;
              void* m_pHeadFree;
              void* m_pEnd;
          };
      

      不用担心我继承了什么。我一直在将此分配器与内存池结合使用。但基本上不是从 operator new 获取内存,而是从内存池中获取内存。内部工作原理基本相同。

      这里是实现:

      LinearAllocator::LinearAllocator():ObjectBase::ObjectBase()
      {
          m_pBlock = nullptr;
          m_pHeadFree = nullptr;
          m_pEnd=nullptr;
      }
      
      LinearAllocator::LinearAllocator(Pool* pool,size_t size):ObjectBase::ObjectBase(pool)
      {
          if (pool!=nullptr) {
              m_pBlock = ObjectBase::AllocFromPool(size);
              m_pHeadFree = * m_pBlock;
              m_pEnd = (void*)((unsigned char*)*m_pBlock+size);
          }
          else{
              m_pBlock = nullptr;
              m_pHeadFree = nullptr;
              m_pEnd=nullptr;
          }
      }
      LinearAllocator::~LinearAllocator()
      {
          if (m_pBlock!=nullptr) {
              ObjectBase::FreeFromPool(m_pBlock);
          }
          m_pBlock = nullptr;
          m_pHeadFree = nullptr;
          m_pEnd=nullptr;
      }
      MemoryBlock* LinearAllocator::Alloc(size_t size)
      {
          if (m_pBlock!=nullptr) {
              void* test = (void*)((unsigned char*)m_pEnd-size);
              if (m_pHeadFree<=test) {
                  void* temp = m_pHeadFree;
                  m_pHeadFree=(void*)((unsigned char*)m_pHeadFree+size);
                  return temp;
              }else{
                  return nullptr;
              }
          }else return nullptr;
      }
      void LinearAllocator::Flush()
      {
          if (m_pBlock!=nullptr) {
              m_pHeadFree=m_pBlock;
              size_t size = (unsigned char*)m_pEnd-(unsigned char*)*m_pBlock;
              memset(*m_pBlock,0,size);
          }
      }
      

      此代码功能齐全,除了由于我继承和使用内存池而需要更改的几行。但我敢打赌,您可以弄清楚需要更改的内容,如果您需要手动更改代码,请告诉我。此代码尚未在任何类型的专业庄园中进行过测试,并且不能保证是线程安全的或类似的任何花哨的东西。我只是把它搅起来,想我可以和你分享,因为你似乎需要帮助。

      如果您认为它可以帮助您,我也有一个完全通用的内存池的工作实现。如果您需要,我可以解释它是如何工作的。

      如果您需要任何帮助,请再次告诉我。祝你好运。

      【讨论】:

      • 步骤 3.5:如果要求为不同类型的对象分配空间,您将担心您的 alloc() 函数是否正确处理对齐问题。
      【解决方案4】:

      您的想法很棒,数以百万计的应用程序已经在使用它。这种模式最著名的是«autorelease pool»。它为 Cocoa 和 Cocoa Touch Objective-C 框架中的“智能”内存管理奠定了基础。尽管 C++ 提供了很多其他的替代方案,但我仍然认为这个想法有很多好处。但我认为您的实施可能在某些方面存在不足。

      我能想到的第一个问题是线程安全。例如,当从不同线程创建相同基础的对象时会发生什么?一种解决方案可能是使用互斥锁保护池访问。虽然我认为更好的方法是使该池成为特定于线程的对象。

      第二个问题是在派生类的构造函数抛出异常的情况下调用未定义的行为。你看,如果发生这种情况,派生对象将不会被构造,但你的B 的构造函数已经将指向this 的指针推送到向量。稍后,当向量被清除时,它会尝试通过对象的虚拟表调用析构函数,该对象要么不存在,要么实际上是不同的对象(因为new 可以重用该地址)。

      我不喜欢的第三件事是您只有一个全局池,即使它是特定于线程的,也不允许对分配对象的范围进行更细粒度的控制。

      考虑到上述情况,我会做一些改进:

      1. 拥有一组池以进行更细粒度的范围控制。
      2. 使该池堆栈成为特定于线程的对象。
      3. 如果发生故障(例如派生类构造函数中的异常),请确保池中没有悬空指针。

      这是我真正的 5 分钟解决方案,不要判断快速和肮脏:

      #include <new>
      #include <set>
      #include <stack>
      #include <cassert>
      #include <memory>
      #include <stdexcept>
      #include <iostream>
      
      #define thread_local __thread // Sorry, my compiler doesn't C++11 thread locals
      
      struct AutoReleaseObject {
          AutoReleaseObject();
          virtual ~AutoReleaseObject();
      };
      
      class AutoReleasePool final {
        public:
          AutoReleasePool() {
              stack_.emplace(this);
          }
      
          ~AutoReleasePool() noexcept {
              std::set<AutoReleaseObject *> obj;
              obj.swap(objects_);
              for (auto *p : obj) {
                  delete p;
              }
              stack_.pop();
          }
      
          static AutoReleasePool &instance() {
              assert(!stack_.empty());
              return *stack_.top();
          }
      
          void add(AutoReleaseObject *obj) {
              objects_.insert(obj);
          }
      
          void del(AutoReleaseObject *obj) {
              objects_.erase(obj);
          }
      
          AutoReleasePool(const AutoReleasePool &) = delete;
          AutoReleasePool &operator = (const AutoReleasePool &) = delete;
      
        private:
          // Hopefully, making this private won't allow users to create pool
          // not on stack that easily... But it won't make it impossible of course.
          void *operator new(size_t size) {
              return ::operator new(size);
          }
      
          std::set<AutoReleaseObject *> objects_;
      
          struct PrivateTraits {};
      
          AutoReleasePool(const PrivateTraits &) {
          }
      
          struct Stack final : std::stack<AutoReleasePool *> {
              Stack() {
                  std::unique_ptr<AutoReleasePool> pool
                      (new AutoReleasePool(PrivateTraits()));
                  push(pool.get());
                  pool.release();
              }
      
              ~Stack() {
                  assert(!stack_.empty());
                  delete stack_.top();
              }
          };
      
          static thread_local Stack stack_;
      };
      
      thread_local AutoReleasePool::Stack AutoReleasePool::stack_;
      
      AutoReleaseObject::AutoReleaseObject()
      {
          AutoReleasePool::instance().add(this);
      }
      
      AutoReleaseObject::~AutoReleaseObject()
      {
          AutoReleasePool::instance().del(this);
      }
      
      // Some usage example...
      
      struct MyObj : AutoReleaseObject {
          MyObj() {
              std::cout << "MyObj::MyObj(" << this << ")" << std::endl;
          }
      
          ~MyObj() override {
              std::cout << "MyObj::~MyObj(" << this << ")" << std::endl;
          }
      
          void bar() {
              std::cout << "MyObj::bar(" << this << ")" << std::endl;
          }
      };
      
      struct MyObjBad final : AutoReleaseObject {
          MyObjBad() {
              throw std::runtime_error("oops!");
          }
      
          ~MyObjBad() override {
          }
      };
      
      void bar()
      {
          AutoReleasePool local_scope;
          for (int i = 0; i < 3; ++i) {
              auto o = new MyObj();
              o->bar();
          }
      }
      
      void foo()
      {
          for (int i = 0; i < 2; ++i) {
              auto o = new MyObj();
              bar();
              o->bar();
          }
      }
      
      int main()
      {
          std::cout << "main start..." << std::endl;
          foo();
          std::cout << "main end..." << std::endl;
      }
      

      【讨论】:

        【解决方案5】:

        嗯,我最近需要几乎完全相同的东西(程序的一个阶段的内存池一次全部清除),除了我有额外的设计约束,我的所有对象都相当小。

        我想出了以下“小对象内存池”——也许它对你有用:

        #pragma once
        
        #include "defs.h"
        #include <cstdint>      // uintptr_t
        #include <cstdlib>      // std::malloc, std::size_t
        #include <type_traits>  // std::alignment_of
        #include <utility>      // std::forward
        #include <algorithm>    // std::max
        #include <cassert>      // assert
        
        
        // Small-object allocator that uses a memory pool.
        // Objects constructed in this arena *must not* have delete called on them.
        // Allows all memory in the arena to be freed at once (destructors will
        // be called).
        // Usage:
        //     SmallObjectArena arena;
        //     Foo* foo = arena::create<Foo>();
        //     arena.free();        // Calls ~Foo
        class SmallObjectArena
        {
        private:
            typedef void (*Dtor)(void*);
        
            struct Record
            {
                Dtor dtor;
                short endOfPrevRecordOffset;    // Bytes between end of previous record and beginning of this one
                short objectOffset;             // From the end of the previous record
            };
        
            struct Block
            {
                size_t size;
                char* rawBlock;
                Block* prevBlock;
                char* startOfNextRecord;
            };
        
            template<typename T> static void DtorWrapper(void* obj) { static_cast<T*>(obj)->~T(); }
        
        public:
            explicit SmallObjectArena(std::size_t initialPoolSize = 8192)
                : currentBlock(nullptr)
            {
                assert(initialPoolSize >= sizeof(Block) + std::alignment_of<Block>::value);
                assert(initialPoolSize >= 128);
        
                createNewBlock(initialPoolSize);
            }
        
            ~SmallObjectArena()
            {
                this->free();
                std::free(currentBlock->rawBlock);
            }
        
            template<typename T>
            inline T* create()
            {
                return new (alloc<T>()) T();
            }
        
            template<typename T, typename A1>
            inline T* create(A1&& a1)
            {
                return new (alloc<T>()) T(std::forward<A1>(a1));
            }
        
            template<typename T, typename A1, typename A2>
            inline T* create(A1&& a1, A2&& a2)
            {
                return new (alloc<T>()) T(std::forward<A1>(a1), std::forward<A2>(a2));
            }
        
            template<typename T, typename A1, typename A2, typename A3>
            inline T* create(A1&& a1, A2&& a2, A3&& a3)
            {
                return new (alloc<T>()) T(std::forward<A1>(a1), std::forward<A2>(a2), std::forward<A3>(a3));
            }
        
            // Calls the destructors of all currently allocated objects
            // then frees all allocated memory. Destructors are called in
            // the reverse order that the objects were constructed in.
            void free()
            {
                // Destroy all objects in arena, and free all blocks except
                // for the initial block.
                do {
                    char* endOfRecord = currentBlock->startOfNextRecord;
                    while (endOfRecord != reinterpret_cast<char*>(currentBlock) + sizeof(Block)) {
                        auto startOfRecord = endOfRecord - sizeof(Record);
                        auto record = reinterpret_cast<Record*>(startOfRecord);
                        endOfRecord = startOfRecord - record->endOfPrevRecordOffset;
                        record->dtor(endOfRecord + record->objectOffset);
                    }
        
                    if (currentBlock->prevBlock != nullptr) {
                        auto memToFree = currentBlock->rawBlock;
                        currentBlock = currentBlock->prevBlock;
                        std::free(memToFree);
                    }
                } while (currentBlock->prevBlock != nullptr);
                currentBlock->startOfNextRecord = reinterpret_cast<char*>(currentBlock) + sizeof(Block);
            }
        
        private:
            template<typename T>
            static inline char* alignFor(char* ptr)
            {
                const size_t alignment = std::alignment_of<T>::value;
                return ptr + (alignment - (reinterpret_cast<uintptr_t>(ptr) % alignment)) % alignment;
            }
        
            template<typename T>
            T* alloc()
            {
                char* objectLocation = alignFor<T>(currentBlock->startOfNextRecord);
                char* nextRecordStart = alignFor<Record>(objectLocation + sizeof(T));
                if (nextRecordStart + sizeof(Record) > currentBlock->rawBlock + currentBlock->size) {
                    createNewBlock(2 * std::max(currentBlock->size, sizeof(T) + sizeof(Record) + sizeof(Block) + 128));
                    objectLocation = alignFor<T>(currentBlock->startOfNextRecord);
                    nextRecordStart = alignFor<Record>(objectLocation + sizeof(T));
                }
                auto record = reinterpret_cast<Record*>(nextRecordStart);
                record->dtor = &DtorWrapper<T>;
                assert(objectLocation - currentBlock->startOfNextRecord < 32768);
                record->objectOffset = static_cast<short>(objectLocation - currentBlock->startOfNextRecord);
                assert(nextRecordStart - currentBlock->startOfNextRecord < 32768);
                record->endOfPrevRecordOffset = static_cast<short>(nextRecordStart - currentBlock->startOfNextRecord);
                currentBlock->startOfNextRecord = nextRecordStart + sizeof(Record);
        
                return reinterpret_cast<T*>(objectLocation);
            }
        
            void createNewBlock(size_t newBlockSize)
            {
                auto raw = static_cast<char*>(std::malloc(newBlockSize));
                auto blockStart = alignFor<Block>(raw);
                auto newBlock = reinterpret_cast<Block*>(blockStart);
                newBlock->rawBlock = raw;
                newBlock->prevBlock = currentBlock;
                newBlock->startOfNextRecord = blockStart + sizeof(Block);
                newBlock->size = newBlockSize;
                currentBlock = newBlock;
            }
        
        private:
            Block* currentBlock;
        };
        

        要回答您的问题,您不会调用未定义的行为,因为在完全构造对象之前没有人使用指针(在此之前指针值本身可以安全地复制)。然而,这是一种相当侵入性的方法,因为对象本身需要了解内存池。此外,如果您正在构建大量小对象,使用实际的内存池(就像我的池一样)可能会更快,而不是为每个对象调用 new

        无论您使用什么类似池的方法,请注意不要手动处理对象deleteed,因为这会导致双重释放!

        【讨论】:

          猜你喜欢
          • 2010-09-24
          • 1970-01-01
          • 2015-04-06
          • 2020-06-15
          • 1970-01-01
          • 2012-04-23
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多