web-dev-qa-db-ja.com

C ++ 11メモリプールのデザインパターン?

私は、すべてが最終的に共通の基本クラスから派生したポリモーフィック型のツリーから、さまざまなオブジェクトインスタンス(すべてヒープに割り当てられている)を使用する必要がある処理フェーズを含むプログラムを持っています。

インスタンスは循環的に相互に参照し、明確な所有者を持たないため、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は、派生クラスコンストラクターが完了する前の、基本クラスコンストラクター内。これにより、未定義の動作が発生しますか?その場合、回避策はありますか?

35
Andrew Tomazos

まだ行っていない場合は、 Boost.Pool に慣れてください。 Boostドキュメントから:

プールとは何ですか?

プール割り当ては、非常に高速ですが、使用量が制限されているメモリ割り当てスキームです。プール割り当て(simplesegregated storageとも呼ばれる)の詳細については、 concepts コンセプトおよび Simple Segregated Storage を参照してください。

プールを使用する理由は何ですか?

プールを使用すると、プログラムでメモリを使用する方法をより詳細に制御できます。たとえば、ある時点で多数の小さなオブジェクトを割り当ててから、プログラム内でそれらのオブジェクトが不要になるポイントに到達したい場合があります。プールインターフェイスを使用して、デストラクタを実行するか、単に忘却に落とすかを選択できます。プールインターフェイスは、システムメモリリークがないことを保証します。

いつプールを使用すべきですか?

通常、プールは、小さなオブジェクトの割り当てと割り当て解除が多い場合に使用されます。別の一般的な使用法は、多くのオブジェクトがメモリからドロップされる上記の状況です。

一般的に、異常なメモリ制御を行うためのより効率的な方法が必要な場合は、プールを使用します。

どのプールアロケーターを使用すべきですか?

pool_allocatorは、より汎用的なソリューションであり、任意の数の連続したチャンクに対するリクエストを効率的に処理することを目的としています。

fast_pool_allocatorも汎用ソリューションですが、一度に1つのチャンクのリクエストを効率的に処理することを目的としています。連続したチャンクに対しては機能しますが、pool_allocatorとは異なります。

パフォーマンスについて真剣に懸念している場合は、fast_pool_allocatorなどのコンテナを扱うときはstd::listを使用し、pool_allocatorなどのコンテナを扱うときはstd::vectorを使用します。

メモリ管理はトリッキーなビジネス(スレッディング、キャッシング、アライメント、断片化など)です。プロダラーがボトルネックを示さない限り、本番のコードでは、適切に設計され慎重に最適化されたライブラリを使用します。

15
TemplateRex

あなたのアイデアは素晴らしく、何百万ものアプリケーションがすでにそれを使用しています。このパターンは、最も有名な「自動解放プール」として知られています。 CocoaおよびCocoa Touch Objective-Cフレームワークの「スマート」メモリ管理の基盤を形成します。 C++が他の多くの代替手段を提供しているという事実にもかかわらず、私はまだこのアイデアには多くの利点があると思います。しかし、あなたの実装が現状のままでは不十分かもしれないと思うことはほとんどありません。

私が考えることができる最初の問題は、スレッドの安全性です。たとえば、同じベースのオブジェクトが異なるスレッドから作成されるとどうなりますか?解決策は、相互に排他的なロックでプールアクセスを保護することです。これを行うより良い方法は、そのプールをスレッド固有のオブジェクトにすることだと思いますが。

2番目の問題は、派生クラスのコンストラクターが例外をスローした場合に未定義の動作を呼び出すことです。その場合、派生オブジェクトは構築されませんが、Bのコンストラクターは既にthisへのポインターをベクトルにプッシュしているはずです。後でベクターがクリアされると、存在しないオブジェクトまたは実際には別のオブジェクトであるオブジェクトの仮想テーブルを介してデストラクタを呼び出そうとします(newはそのアドレスを再利用できるため)。

3番目に気に入らないのは、スレッド固有であっても、割り当てられたオブジェクトのスコープをよりきめ細かく制御できないグローバルプールが1つしかないことです。

上記を考慮して、いくつかの改善を行います。

  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;
}
14
user405725

うーん、最近はほぼ同じもの(一度にすべてクリアされるプログラムの1つのフェーズのメモリプール)が必要でしたが、すべてのオブジェクトがかなり小さくなるという追加の設計制約がありました。

私は次の「小さなオブジェクトのメモリプール」を思いつきました-おそらくあなたにとって役に立つでしょう:

#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されないように注意してください。二重に解放されるためです!

4
Cameron

私はまだこれが明確な回答のない興味深い質問だと思いますが、実際にあなたが尋ねているさまざまな質問に分けてください:

1.)サブクラスの初期化の前に基本クラスへのポインターをベクターに挿入すると、そのポインターから継承されたクラスを取得する際の問題が防止または発生しますか。 [たとえばスライス。]

回答:いいえ、指摘されている関連するタイプが100%確実である限り、このメカニズムはこれらの問題を引き起こしませんが、次の点に注意してください。

派生コンストラクタが失敗した場合、[派生クラス]が取得すると考えていたアドレス空間がオペレーティング環境に解放されると考えられるため、少なくともベクターにダングリングポインターが存在する可能性が高くなります。失敗した場合でも、ベクターには基本クラス型のアドレスがまだあります。

ベクトルは一種の有用ではありますが、これには最適な構造ではないことに注意してください。たとえあったとしても、ベクトルオブジェクトがオブジェクトの初期化を制御できるように、ここで制御の反転が必要です。成功/失敗の。

これらの点は、暗黙の2番目の質問につながります。

2.)これはプーリングに適したパターンですか?

回答:実際には、上記の理由に加えて、その他にも理由があります(ベクトルをその終点を超えてプッシュすると、基本的に不要なmallocになり、パフォーマンスに影響します)。理想的には、プーリングライブラリまたはテンプレートクラスを使用します。さらに良いことに、プールの実装から割り当て/割り当て解除ポリシーの実装を分離します。既に示唆されている低レベルのソリューションは、プールの初期化から適切なプールメモリを割り当て、次にこれを使用してポインタを使用して内部から無効にしますプールアドレススペース(上記のAlex Zywickiのソリューションを参照してください。)このパターンを使用すると、プールの破壊は安全です。アドレスがストレージマネージャーによってプールを介して割り当てられたオブジェクトへのすべての参照を失うと、ダーティチャンクが残りますが、プールimplによって管理されるため、メモリリークは発生しません。浮気。

C/C++の初期(STLが大量に拡散する前)には、これはよく議論されたパターンであり、多くの実装と設計は優れた文献にあります:例:

Knuth(1973 The computer programming of computer:Multiple volume)、およびプーリングに関する詳細なリストについては、以下を参照してください。

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

3番目の暗黙の質問は次のようです。

3)これはプーリングを使用する有効なシナリオですか?

回答:これは、使いやすいものに基づいてローカライズされた設計上の決定ですが、正直なところ、実装(オブジェクトのサブセットの制御構造/集合体、周期的な共有はありません)は、ラッパーオブジェクトの基本リンクリスト。各オブジェクトには、アドレス指定の目的でのみ使用されるスーパークラスへのポインターが含まれています。循環構造はこの上に構築され、必要に応じてすべてのファーストクラスオブジェクトに対応するために必要に応じてリストを修正/拡張し、完了したら、O(1)リンクリスト内からの操作。

そうは言っても、現時点では(プーリングを使用するシナリオがあるため、適切な考え方が必要な場合)、ストレージ管理/プーリングクラスのセットを構築することを個人的にお勧めします。将来のためにあなたをしっかりと支えてくれるようになりました。

3
user2654834

これは、私が聞いた「線形アロケーター」と呼ばれる音です。私はそれがどのように機能するかを理解する方法の基本を説明します。

  1. :: operator new(size);を使用してメモリブロックを割り当てます。
  2. メモリ内の次の空き領域へのポインタであるvoid *を持ちます。
  3. Alloc(size_t size)関数があり、ステップ1からブロック内の位置へのポインターを提供して、配置Newの使用を構築します。
  4. 配置newは次のようになります... int * i = new(location)int(); locationは、アロケータから割り当てたメモリブロックへのvoid *です。
  5. すべてのメモリを使い終わったら、Flush()関数を呼び出して、プールからメモリの割り当てを解除するか、少なくともデータを消去します。

最近、これらの1つをプログラムしました。説明のために最善を尽くすとともに、ここにコードを投稿します。

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

私が何を継承しているのか心配しないでください。私はこのアロケーターをメモリプールと組み合わせて使用​​しています。しかし基本的には、演算子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);
    }
}

このコードは、メモリプールの継承と使用のために変更する必要がある数行を除いて完全に機能します。しかし、何を変更する必要があるかを把握し、コードを変更する必要がある場合はお知らせください。このコードは、いかなる種類の専門家の邸宅でもテストされておらず、スレッドセーフまたはそのような派手なものであることが保証されていません。あなたが助けを必要としているように思えたので、私はそれをただホイップし、あなたとそれを共有できると思った。

また、役立つと思われる場合は、完全に汎用的なメモリプールの実用的な実装もあります。必要に応じてどのように機能するかを説明できます。

もう一度助けが必要な場合はお知らせください。がんばろう。

2
Alex Zywicki