Шаблон проектирования пула памяти С++ 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 в конструкторе базового класса до завершения конструктора производного класса. Приводит ли это к неопределенному поведению? Если да, то есть ли обходной путь?


person Andrew Tomazos    schedule 04.05.2013    source источник
comment
Зачем B нужно знать о пуле памяти? Добавление указателей из кода драйвера (main) кажется неизмеримо более предпочтительным. И тогда, конечно, время жизни пула может быть ограничено, что, в свою очередь, означает, что вы получаете бесплатное уничтожение, когда оно выходит за рамки.   -  person Jon    schedule 04.05.2013
comment
@Jon: Вы имеете в виду заменить вызовы new D(...) на memory_pool.emplace_back(new D(...))? Я полагаю, что мог бы написать шаблонную функцию в стиле make_shared, которая создает объект, передает аргументы конструктору и добавляет его в пул памяти. Я все еще хотел бы знать, является ли приведенный выше код UB или нет.   -  person Andrew Tomazos    schedule 04.05.2013
comment
Чтобы узнать о хорошо известном решении FOSS, ознакомьтесь с консервативным сборщиком мусора C/C++ Boehm-Demers-Weiser. .   -  person    schedule 03.05.2018


Ответы (5)


Если вы еще этого не сделали, ознакомьтесь с Boost.Pool. Из документации Boost:

Что такое пул?

Распределение пула — это очень быстрая схема выделения памяти, но ее использование ограничено. Для получения дополнительной информации о распределении пула (также называемого простым отдельным хранилищем) см. концепции и Простое раздельное хранилище.

Зачем мне использовать пул?

Использование пулов дает вам больше контроля над тем, как память используется в вашей программе. Например, у вас может быть ситуация, когда вы хотите выделить кучу небольших объектов в одной точке, а затем достичь точки в вашей программе, где ни один из них больше не нужен. Используя интерфейсы пула, вы можете запускать их деструкторы или просто бросать их в небытие; интерфейс пула гарантирует отсутствие утечек системной памяти.

Когда следует использовать пул?

Пулы обычно используются, когда есть много выделения и освобождения небольших объектов. Другим распространенным использованием является описанная выше ситуация, когда многие объекты могут быть выброшены из памяти.

В общем, используйте пулы, когда вам нужен более эффективный способ необычного управления памятью.

Какой распределитель пула следует использовать?

pool_allocator — это решение более общего назначения, ориентированное на эффективное обслуживание запросов для любого количества смежных фрагментов.

fast_pool_allocator также является решением общего назначения, но оно ориентировано на эффективное обслуживание запросов по одному фрагменту за раз; он будет работать для смежных фрагментов, но не так хорошо, как pool_allocator.

Если вас серьезно беспокоит производительность, используйте fast_pool_allocator при работе с контейнерами, такими как std::list, и используйте pool_allocator при работе с контейнерами, такими как std::vector.

Управление памятью — сложная задача (потоки, кэширование, выравнивание, фрагментация и т. д. и т. д.). Для серьезного производственного кода лучше всего использовать хорошо спроектированные и тщательно оптимизированные библиотеки, если только ваш профайлер не обнаружит узкое место.

person TemplateRex    schedule 04.05.2013

Ваша идея великолепна, и миллионы приложений уже используют ее. Этот паттерн наиболее известен как «пул авторелиза». Он формирует основу для «умного» управления памятью в платформах 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;
}
person Community    schedule 04.05.2013

Хм, недавно мне понадобилось почти то же самое (пул памяти для одной фазы программы, которая очищается сразу), за исключением того, что у меня было дополнительное конструктивное ограничение, заключающееся в том, что все мои объекты должны быть довольно маленькими.

Я придумал следующий "пул памяти для небольших объектов" - возможно, он будет вам полезен:

#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 вручную, потому что это приведет к двойному освобождению!

person Cameron    schedule 04.05.2013

Я все еще думаю, что это интересный вопрос без окончательного ответа, но, пожалуйста, позвольте мне разбить его на различные вопросы, которые вы на самом деле задаете:

1.) Вставка указателя на базовый класс в вектор до инициализации подкласса предотвращает или вызывает проблемы с получением унаследованных классов от этого указателя. [нарезка, например.]

Ответ: Нет, пока вы на 100 % уверены в соответствующем типе, на который указывает указатель, этот механизм не вызывает этих проблем, однако обратите внимание на следующие моменты:

Если производный конструктор дает сбой, вы остаетесь с проблемой позже, когда у вас, вероятно, будет висячий указатель, по крайней мере, сидящий в векторе, поскольку это адресное пространство, которое он [производный класс] думал, что оно получает, будет освобождено для операционной среды. в случае неудачи, но вектор по-прежнему имеет адрес типа базового класса.

Обратите внимание, что вектор, хотя и полезный, не является лучшей структурой для этого, и даже если бы это было так, здесь должна быть задействована некоторая инверсия управления, чтобы позволить векторному объекту управлять инициализацией ваших объектов, чтобы вы знали успеха/неудачи.

Эти пункты приводят к подразумеваемому второму вопросу:

2.) Это хороший шаблон для объединения?

Ответ: Не совсем, по причинам, упомянутым выше, а также по другим причинам (проталкивание вектора за его конечную точку в основном заканчивается ненужным malloc и влияет на производительность). В идеале вы хотите использовать библиотеку пула или класс шаблона, и, что еще лучше, отделите реализацию политики выделения/освобождения от реализации пула, с уже намекающим на низкоуровневое решение, которое заключается в выделении адекватной памяти пула из инициализации пула, а затем используйте это с помощью указателей для пустоты изнутри адресное пространство пула (см. решение Alex Zywicki выше.) Используя этот шаблон, уничтожение пула безопасно, поскольку пул, который будет непрерывной памятью, может быть уничтожен в массовом порядке без каких-либо висячих проблем или утечек памяти из-за потери всех ссылок на объект ( потеря всех ссылок на объект, адрес которого выделяется через пул диспетчером хранения, оставляет вас с грязными фрагментами, но не вызовет утечку памяти, поскольку он управляется пулом. ментация.

На заре C/C++ (до массового распространения STL) это был хорошо обсуждаемый шаблон, и в хорошей литературе можно найти множество реализаций и дизайнов: Например:

Кнут (1973 Искусство компьютерного программирования: несколько томов), а более полный список, в том числе о объединении, см.:

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

Третий подразумеваемый вопрос выглядит следующим образом:

3) Является ли это допустимым сценарием для использования пула?

Ответ: Это локализованное дизайнерское решение, основанное на том, что вам удобно, но, честно говоря, ваша реализация (отсутствие управляющей структуры/агрегата, возможно, циклическое совместное использование подмножеств объектов) подсказывает мне, что вам было бы лучше использовать базовый связанный список объектов-оболочек, каждый из которых содержит указатель на ваш суперкласс, используемый только для целей адресации. Ваши циклические структуры построены поверх этого, и вы просто изменяете/расширяете список по мере необходимости, чтобы вместить все ваши объекты первого класса по мере необходимости, а когда закончите, вы можете легко уничтожить их в эффективной операции O (1) из связанного списка.

Сказав это, я бы лично порекомендовал в это время (когда у вас есть сценарий, в котором пул действительно используется, и поэтому вы находитесь в правильном мышлении) выполнить создание набора классов управления хранилищем/пула, которые являются параматеризованными/бестиповыми сейчас, так как это сослужит вам хорошую службу в будущем.

person Community    schedule 22.10.2014

Это звучит так, как я слышал, под названием Linear Allocator. Я объясню основы того, как я понимаю, как это работает.

  1. Выделить блок памяти с помощью ::operator new(size);
  2. У вас есть void*, который является вашим указателем на следующее свободное место в памяти.
  3. У вас будет функция alloc(size_t size), которая даст вам указатель на местоположение в блоке с первого шага, чтобы вы могли построить его с помощью Placement New.
  4. Размещение new выглядит так... 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);
    }
}

Этот код полностью функционален, за исключением нескольких строк, которые нужно будет изменить из-за моего наследования и использования пула памяти. но я уверен, что вы можете понять, что нужно изменить, и просто дайте мне знать, если вам нужна помощь, чтобы изменить код. Этот код не тестировался в какой-либо профессиональной усадьбе, и не гарантируется, что он будет потокобезопасным или чем-то подобным. я просто на скорую руку подумал, что могу поделиться с вами, так как вам, похоже, нужна помощь.

У меня также есть рабочая реализация полностью универсального пула памяти, если вы думаете, что это может вам помочь. Я могу объяснить, как это работает, если вам нужно.

Еще раз, если вам нужна помощь, дайте мне знать. Удачи.

person Alex Zywicki    schedule 25.01.2014
comment
Шаг 3.5: вы будете беспокоиться о том, правильно ли ваша функция alloc() обрабатывает проблемы с выравниванием, если ее просят выделить пространство для различных типов объектов. - person Ron Burk; 12.11.2015