簡體   English   中英

C ++ 11內存池設計模式?

[英]C++11 memory pool design pattern?

我有一個程序,其中包含一個處理階段,該階段需要使用多態類型樹中的一堆不同的對象實例(全部分配在堆上),所有這些實例最終都來自一個公共基類。

由於實例可能會相互循環引用,並且沒有明確的所有者,因此我想為它們分配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 這會導致不確定的行為嗎? 如果是這樣,是否有解決方法?

如果您還沒有熟悉,請熟悉Boost.Pool 從Boost文檔中:

什么是游泳池?

池分配是一種非常快速的內存分配方案,但是其使用受到限制。 有關池分配(也稱為簡單隔離存儲)的更多信息,請參閱概念概念和簡單隔離存儲

為什么要使用Pool?

使用池使您可以更好地控制程序中內存的使用方式。 例如,您可能會遇到這樣的情況,您希望一次分配一堆小對象,然后到達程序中不再需要它們的一點。 使用池接口,您可以選擇運行它們的析構函數,或者只是將其丟棄。 池接口將確保沒有系統內存泄漏。

我什么時候應該使用游泳池?

當小對象有大量分配和釋放時,通常使用池。 另一個常見用法是上述情況,其中許多對象可能會從內存中丟失。

通常,當您需要更有效的方式執行異常內存控制時,請使用池。

我應該使用哪個池分配器?

pool_allocator是更通用的解決方案,旨在有效地服務於任意數量的連續塊的請求。

fast_pool_allocator也是一種通用解決方案,但旨在有效地一次處理一個塊的請求。 它適用於連續的塊,但不如pool_allocator

如果您非常擔心性能, fast_pool_allocator在處理諸如std::list容器時使用pool_allocator ,而在處理諸如std::vector容器時使用pool_allocator

內存管理是一項棘手的業務(線程,緩存,對齊,分段等),對於嚴肅的生產代碼,除非您的分析器顯示出瓶頸,否則經過精心設計和精心優化的庫是必經之路。

您的想法很棒,已經有數百萬個應用程序在使用它。 這種模式最著名的是“自動釋放池”。 它構成了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;
}

嗯,最近我需要幾乎完全相同的東西(一個程序的一個階段的內存池立即被清除),除了我還有其他設計約束,即我的所有對象都非常小。

我提出了以下“小對象內存池”-也許對您有用:

#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對象,因為這將導致雙重釋放!

我仍然認為這是一個有趣的問題,沒有明確的答復,但請允許我將其分解為您實際提出的不同問題:

1.)在子類初始化之前,是否將指向基類的指針插入向量中,是否會防止或導致從該指針檢索繼承的類引起問題。 [例如切片]

答:不,只要您100%確定所指向的相關類型,此機制就不會引起這些問題,但是請注意以下幾點:

如果派生的構造函數失敗,則稍后可能會出現一個問題,即您可能至少在向量中有一個懸空指針,因為它[派生類]認為該地址空間將被釋放到操作環境中如果失敗,則向量仍然具有基類類型的地址。

請注意,向量雖然有用,但並不是最佳的結構,即使是向量,這里也應該包含一些控制反轉,以允許向量對象控制對象的初始化,以便您有意識成功/失敗。

這些觀點引出了隱含的第二個問題:

2.)這是合並的好模式嗎?

答:並非完全出於上述原因,還有其他原因(將向量推過端點之后基本上會產生malloc,這是不必要的,並且會影響性​​能。)理想情況下,您想使用池化庫或模板類,甚至更好的是,將分配/取消分配策略實現與池實現分離開來,已經暗示了一個低級解決方案,即從池初始化中分配足夠的池內存,然后使用該指針通過指針從內部進行清空池地址空間(請參見上面的Alex Zywicki的解決方案。)使用這種模式,池的銷毀是安全的,因為可以連續銷毀連續內存的池而不會出現任何懸空問題,或者由於丟失對對象的所有引用而導致內存泄漏(丟失對對象的所有引用,該對象的地址由存儲管理器通過池分配,會使您的數據塊變臟,但不會導致內存泄漏,因為它由池impl管理 凝結。

在C / C ++的早期(在STL大量傳播之前),這是一個經過充分討論的模式,並且可以在大量文獻中找到許多實現和設計:例如:

Knuth(1973年,計算機程序設計藝術:多卷本),以及更完整的列表,以及有關池的更多信息,請參見:

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

第三個隱含的問題似乎是:

3)這是使用池的有效方案嗎?

答:這是一個基於您滿意的本地化設計決策,但老實說,您的實現(沒有控制結構/集合,可能是對象子集的循環共享)向我建議您最好使用包裝對象的基本鏈接列表,每個包裝對象都包含一個指向您的超類的指針,僅用於尋址目的。 您的周期性結構是基於此構建的,您只需根據需要修改/增加收縮列表即可容納所需的所有一流對象,完成后,您可以輕松地通過O(1)操作輕松銷毀它們從鏈接列表中。

話雖如此,我個人建議此時(當您有使用池的情況,因此您處於正確的思維模式時)進行存儲管理/池化類集的構建,現在已成為准寄生物/無類型毒物,因為它將為您帶來未來的穩定。

這聽起來像我聽說的線性分配器。 我將解釋如何理解其工作原理。

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

不用擔心我繼承的東西。 我一直在與內存池一起使用此分配器。 但是基本上不是從操作員那里獲取內存,而是從內存池獲取內存。 內部工作原理基本相同。

這是實現:

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

該代碼具有全部功能,但由於我繼承和使用了內存池,因此需要更改的幾行代碼。 但是我敢打賭,您可以弄清楚需要更改的內容,如果您需要手動更改代碼,請告訴我。 該代碼尚未在任何類型的專業庄園中進行過測試,不能保證是線程安全的或類似的東西。 我只是鞭打了一下,以為可以與您分享,因為您似乎需要幫助。

如果您認為它可以為您提供幫助,我也可以使用一個完全通用的內存池。 如果您需要,我可以解釋它是如何工作的。

如果您需要任何幫助,請再次告訴我。 祝好運。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM