首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >C++11内存池设计模式?

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

Stack Overflow用户
提问于 2013-05-05 03:50:57
回答 5查看 43.3K关注 0票数 39

我有一个程序,它包含一个处理阶段,它需要使用来自多态类型树的一组不同的对象实例(都分配在堆上),所有这些对象实例最终都派生于一个公共基类。

由于这些实例可能会循环引用彼此,并且没有明确的所有者,因此我希望为它们分配new,使用原始指针处理它们,并将它们留在内存中用于该阶段(即使它们成为未引用的),然后在使用这些实例的程序阶段结束后,我希望一次性将它们全部删除。

我认为如何组织它如下所示:

代码语言:javascript
复制
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。这是否会导致未定义的行为?如果是这样的话,有什么解决办法吗?

EN

回答 5

Stack Overflow用户

发布于 2013-05-05 04:51:15

如果您还没有熟悉,请先熟悉一下。Boost文档中:

什么是池?

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

为什么应该使用池?

使用池可以让您更好地控制程序中内存的使用方式。例如,您可能会遇到这样一种情况:您希望在某个点上分配一堆小对象,然后在程序中到达一个不再需要它们的点。使用池接口,您可以选择运行它们的析构函数,或者直接将它们丢弃;池接口将保证没有系统内存泄漏。

什么时候应该使用池?

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

通常,当您需要一种更有效的方式来执行不寻常的内存控制时,请使用池。

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

pool_allocator是一种更通用的解决方案,旨在高效地服务于任意数量的连续块的请求。

fast_pool_allocator也是一种通用的解决方案,但它适合于一次有效地为一个块的请求提供服务;它将适用于连续的块,但不如pool_allocator好。

如果您非常关心性能,请在处理std::list等容器时使用fast_pool_allocator,在处理std::vector等容器时使用pool_allocator

内存管理是一项棘手的业务(线程、缓存、对齐、碎片等)对于严肃的生产代码,除非您的分析器显示出瓶颈,否则设计良好并仔细优化的库是可行的。

票数 18
EN

Stack Overflow用户

发布于 2013-05-05 05:30:07

你的想法很棒,数以百万计的应用程序已经在使用它。这种模式最著名的就是«自动释放池»。它为Cocoa和Cocoa Touch Objective-C框架中的“智能”内存管理奠定了基础。尽管C++提供了许多其他选择的地狱,但我仍然认为这个想法有很多好处。但我认为你现在的实现可能会有一些不足之处。

我能想到的第一个问题是线程安全。例如,当从不同的线程创建相同库的对象时,会发生什么情况?一种解决方案可能是使用互斥锁保护池访问。不过,我认为更好的方法是将该池设置为线程特定的对象。

第二个问题是在派生类的构造函数抛出异常的情况下调用未定义的行为。你看,如果发生这种情况,派生的对象将不会被构造,但你的B的构造函数已经将一个指向this的指针推送到向量。稍后,当向量被清除时,它会试图通过一个不存在的或实际上是不同对象的对象的虚拟表来调用析构函数(因为new可以重用该地址)。

我不喜欢的第三件事是,你只有一个全局池,即使它是特定于线程的,这也不允许对已分配对象的作用域进行更细粒度的控制。

考虑到以上几点,我会做一些改进:

  1. 有一个用于更细粒度作用域控制的池堆栈。
  2. 使池堆栈成为特定于线程的对象。
  3. 如果发生故障(如派生类构造函数中的异常),请确保池不包含悬空指针。

这就是我的5分钟解决方案,不要因为快速和肮脏而判断:

代码语言:javascript
复制
#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;
}
票数 15
EN

Stack Overflow用户

发布于 2013-05-05 04:34:25

嗯,我最近需要几乎完全相同的东西(程序的一个阶段的内存池,它一次被清除),除了我有一个额外的设计约束,我的所有对象都相当小。

我想出了下面的“小对象内存池”--也许它会对您有用:

代码语言:javascript
复制
#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)!

票数 4
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/16378306

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档