C ++ 11内存池的设计模式?(C++11 memory pool design pattern?

2019-09-01 23:17发布

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

作为实例可循环相互引用,并没有一个明确的所有者,我想和分配他们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实例与新的分配,而且没有人内存池后,使用任何指针他们被清除,有没有这个执行上的问题?

特别是我关心的是,其实this指针被用于构建std::unique_ptr在基类的构造函数,派生类的构造函数完成之前。 这是否导致未定义的行为? 如果是的话有一种解决方法?

Answer 1:

你的想法是伟大的,数以百万计的应用程序已经在使用它。 这种模式是最有名的被称为«自动释放池»。 它形成于可可和可可触摸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;
}


Answer 2:

如果你还没有,熟悉Boost.Pool 。 从Boost文档:

什么是池?

池分配是内存分配方案,这是非常快,但它的使用受到限制。 有关池分配更多的信息(也称为简单分隔式 ,看概念概念和简单的分隔式 。

为什么要使用池?

使用池让你在内存如何在程序中使用更多的控制。 例如,你可以有一个情况下,你要在一个点分配了一堆小物件,然后在你的程序达到一个地步,需要他们没有任何更多。 使用游泳池的接口,你可以选择运行自己的析构函数或者只是把它们送过去被遗忘; 池接口将保证没有系统内存泄漏。

什么时候应该使用池?

池一般都使用时有很多分配和小物件释放的。 另一种常见的用法是上面的情况,其中的对象可能会被丢弃的内存不足。

在一般情况下,当你需要一个更有效的方式做到不寻常的内存控制使用的池。

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

pool_allocator是一种更通用的解决方案,向用于任何数目的连续块的有效服务请求减速。

fast_pool_allocator也是通用的解决方案,但朝向用于在一次一个数据块有效地服务请求齿轮; 它将为连续块的工作,但不作为以及pool_allocator

如果你是认真地关注性能,使用fast_pool_allocator用的容器,如打交道时std::list ,并使用pool_allocator用的容器,如打交道时std::vector

内存管理是棘手的事(线程,缓存,定位,碎片,等等,等等),对于严重的生产代码,精心设计和精心优化图书馆去,除非你的分析器展示了一个瓶颈的方式。



Answer 3:

嗯,我需要几乎一模一样的事情最近(对于被一次性全部清除程序的一相内存池),但我有额外的设计限制,我所有的对象将是相当小的。

我想出了下面的“小对象内存池” - 也许这将是对你有用的:

#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每一个对象。

无论您使用的池式的方法,要注意的是,对象不会手动delete版,因为这将导致双免费的!



Answer 4:

我仍然认为这是没有一个明确的答复了一个有趣的问题,但请让我把它分解成你实际上是在问不同的问题:

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

答:没有,只要你确信被指向到相关类型的100%,这个机制不会导致但是这些问题注意以下几点:

如果派生构造失败,你只剩下后,当你很可能有一个悬摆指针至少坐在载体,因为它[派生类]认为这是越来越会被释放到操作环境的地址空间中的问题失败,但向量仍具有地址为基类型。

需要注意的是一个矢量,虽然那种有用的,是不是这样做的最好的结构,即使是,这里应该有参与,使矢量对象来控制你的对象的初始化控制反转,使你有意识的成功/失败。

这点导致隐含第二个问题:

2)这是汇集了很好的模式?

答:(推矢量过去它的终点基本上用的malloc这是不必要的,并且会影响性​​能结束)不是真的,对于上述原因,加上其他人在理想情况下,你要使用池库或模板类,甚至更好,从池实现分离的分配/解分配政策实施了,与已经在暗示一个较低级的解决方案,这是从池中初始化分配足够的池内存中,然后使用指针从内到无效使用池中的地址空间(见亚历克斯Zywicki的解决方案上面。)使用此模式,池破坏是安全的,因为游泳池,这将是连续的内存可以被毁灭,集体没有任何晃来晃去的问题,或内存泄漏通过失去所有对象引用(失去所有的对象引用,其地址通过池由存储管理器分配给你留下脏块/秒,但因为它是由池IMPL管理,不会造成内存泄漏 ementation。

在C / C的初期++(前的STL的质量增殖),这是一个良好的讨论图案和许多实现和设计可在良好的文献在那里发现:作为一个例子:

高德纳(1973年计算机程序设计艺术:多卷),以及一个更完整的清单,还有更多池,请参阅:

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

第三隐含的问题似乎是:

3)这是使用池一个有效的方案?

答:这是基于你是舒服什么本地化的设计决策,但说实话,你的实现(无控制结构/骨料的子对象集可能循环共享)建议,我认为你会用更好包装对象,每个都包含一个指向超类的基本链接列表,仅用于寻址目的。 您的周期性结构都是建立在此之上,你只需修改/增长,因为需要为需要,以适应所有的第一类型的对象,并在结束时,你就可以轻易摧毁它们有效的O(1)操作收缩名单从链表中。

话虽如此,我个人建议,在这个时候(当你有这样一个场景,汇集确实有使用,所以你是在正确的思维定)来进行存储管理的建筑/池类组在paramaterised /无类型的现在,因为它会抱着你非常有利的未来。



Answer 5:

这听起来我曾经听说过一个叫线性分配器。 我将解释我是如何理解它是如何工作的基本知识。

  1. 分配使用::操作者新的(大小)的存储器的块;
  2. 有一个void *那是你的指针指向内存中的下一个自由空间。
  3. 你将有一个页头(为size_t大小)的功能,这将使你的指针位置,在该块从步骤之一,可以构建到使用新布局
  4. 安置新模样...... INT * I =新(位置),INT(); 其中,位置是一个void *您从分配器alloced的内存块。
  5. 当你与所有你的记忆做你会调用将dealloc的从池内存或至少抹数据清理同花()函数。

我最近编程的其中之一,我将在这里发布我的代码对你以及尽我所能来解释。

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

不用担心什么,我继承。 我一直在使用同一个内存池配合这个分配器。 但基本上而不是从运营商获得新的记忆我从一个存储池中得到记忆。 内部工作是相同的本质。

下面是执行:

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

此代码是除了几行,这将需要因为我的继承权和使用的内存池的改变充分发挥作用。 但我敢打赌,你可以计算出需要改变什么,只是让我知道如果你需要一只手改变代码。 此代码没有任何形式的专业庄园进行了测试,并不能保证是线程安全的或任何幻想这样的。 我刚掀起了起来,以为我可以和你一起分享,因为你似乎需要帮助。

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

再次,如果您需要任何帮助,让我知道。 祝好运。



文章来源: C++11 memory pool design pattern?