C++ 11内存池设计模式?

And*_*zos 35 c++ memory-pool 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

    ...
}
Run Code Online (Sandbox Code Playgroud)

除了小心所有B实例都使用new分配,并且在清除内存池后没有人使用任何指针时,这个实现是否存在问题?

具体来说,我担心在派生类构造函数完成之前,this指针用于std::unique_ptr在基类构造函数中构造a .这是否会导致未定义的行为?如果有的话有解决方法吗?

Tem*_*Rex 15

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

什么是游泳池?

池分配是一种非常快速的内存分配方案,但其使用受到限制.有关池分配的更多信息(也称为简单隔离存储,请参阅概念概念和简单隔离存储.

我为什么要使用Pool?

使用池可以更好地控制程序中内存的使用方式.例如,您可能希望在一个点上分配一堆小对象,然后到达程序中的某个点,不再需要它们.使用池接口,您可以选择运行它们的析构函数,或者只是将其删除为遗忘; 池接口将保证没有系统内存泄漏.

我什么时候应该使用Pool?

当存在大量小对象的分配和释放时,通常使用池.另一种常见用法是上面的情况,其中许多对象可能会从内存中丢失.

通常,在需要更有效的方法来执行异常内存控制时,请使用池.

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

pool_allocator 是一种更通用的解决方案,旨在有效地处理对任意数量的连续块的请求.

fast_pool_allocator也是一种通用的解决方案,但其目的是有效地为一个块的请求提供服务; 它适用于连续的块,但不是很好pool_allocator.

如果您严重关注性能,请 fast_pool_allocator在处理容器时std::list使用,并pool_allocator在处理容器时 使用std::vector.

内存管理是棘手的业务(线程,缓存,对齐,碎片等等).对于严肃的生产代码,精心设计和精心优化的库是可行的方法,除非您的分析器显示瓶颈.


小智 14

您的想法很棒,数百万应用程序已经在使用它.这种模式最着名的是"自动释放池".它构成了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;
}
Run Code Online (Sandbox Code Playgroud)


Cam*_*ron 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;
};
Run Code Online (Sandbox Code Playgroud)

为了回答您的问题,您不会调用未定义的行为,因为在对象完全构造之前没有人使用指针(在此之前指针值本身可以安全地复制)。然而,这是一种相当侵入性的方法,因为对象本身需要了解内存池。此外,如果您正在构造大量小对象,那么使用实际的内存池(就像我的池那样)可能会比调用new每个对象更快。

无论您使用类似池的方法,请注意永远不要手动释放对象delete,因为这会导致双重释放!