С++ 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 выделены новым и что никто не использует никаких указателей на них после очистки пула памяти, существуют ли проблемы с этой реализацией?

В частности, меня беспокоит то, что указатель this используется для построения std::unique_ptr в конструкторе базового класса до завершения конструктора производного класса. Это приводит к поведению undefined? Если это так, обходной путь?

Ответ 1

Ваша идея замечательная, и миллионы приложений уже ее используют. Эта модель наиболее известна как "пул авторезистов". Он формирует базу для "умного" управления памятью в рамках Cocoa и Cocoa Touch Objective-C. Несмотря на то, что С++ предоставляет черту многих других альтернатив, я все еще думаю, что эта идея имеет большой потенциал роста. Но есть несколько вещей, в которых я думаю, что ваша реализация в ее нынешнем виде может оказаться коротким.

Первой проблемой, о которой я могу думать, является безопасность потоков. Например, что происходит, когда объекты одной базы создаются из разных потоков? Решением может быть защита доступа к пулу с помощью взаимоисключающих блокировок. Хотя я думаю, что лучший способ сделать это - сделать этот пул конкретным для потока объектом.

Вторая проблема заключается в вызове поведения undefined в случае, когда конструктор производного класса генерирует исключение. Понимаете, если это произойдет, производный объект не будет создан, но ваш конструктор B уже перенесет указатель на this на вектор. Позже, когда вектор будет очищен, он попытается вызвать деструктор через виртуальную таблицу объекта, которая либо не существует, либо фактически является другим объектом (поскольку new может повторно использовать этот адрес).

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

Учитывая вышеизложенное, я бы сделал несколько улучшений:

  • У вас есть стек пулов для более мелкомасштабного управления областью.
  • Сделайте этот пул стек конкретным объектом.
  • В случае сбоев (например, исключение в конструкторе производного класса) убедитесь, что в пуле нет висячего указателя.

Вот мое буквально 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;
}

Ответ 2

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

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

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

Почему я должен использовать пул?

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

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

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

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

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

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

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

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

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

Ответ 3

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

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

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

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

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

Эти точки приводят к предполагаемому второму вопросу:

2.) Является ли это хорошим шаблоном для объединения?

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

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

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

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

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

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

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

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

Ответ 4

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

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

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

Чтобы ответить на ваш вопрос, вы не вызываете поведение undefined, так как никто не использует указатель до тех пор, пока объект не будет полностью построен (значение самого указателя безопасно копировать до тех пор). Тем не менее, это довольно интрузивный метод, так как сам объект должен знать о пуле памяти. Кроме того, если вы создаете большое количество небольших объектов, скорее всего, будет быстрее использовать фактический пул памяти (например, мой пул) вместо вызова new для каждого объекта.

Каким бы ни был подход, похожий на пул, будьте осторожны, чтобы объекты никогда не были вручную delete ed, потому что это привело бы к двойному свободному!

Ответ 5

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

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

не беспокойтесь о том, что я наследую. Я использовал этот распределитель вместе с пулом памяти. но в основном вместо того, чтобы получать память от оператора 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);
    }
}

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

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

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