39

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

由于实例可能周期性地相互引用,并且没有明确的所有者,我想分配它们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指针用于在基类构造函数中构造 a 。std::unique_ptr这会导致未定义的行为吗?如果是这样,有解决方法吗?

4

5 回答 5

18

如果您还没有,请熟悉Boost.Pool。来自 Boost 文档:

什么是池?

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

为什么要使用池?

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

我应该什么时候使用池?

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

通常,当您需要一种更有效的方法来进行异常内存控制时,请使用池。

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

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

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

如果您非常关心性能,请 fast_pool_allocator在处理诸如 之类的容器时std::list使用,并pool_allocator在处理诸如 std::vector.

内存管理是一项棘手的工作(线程、缓存、对齐、碎片等)。对于严肃的生产代码,精心设计和精心优化的库是必经之路,除非您的分析器证明存在瓶颈。

于 2013-05-04T20:51:14.877 回答
15

你的想法很棒,数以百万计的应用程序已经在使用它。这种模式最著名的是«autorelease pool»。它为 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;
}
于 2013-05-04T21:30:07.073 回答
4

我仍然认为这是一个没有明确答复的有趣问题,但请让我将其分解为您实际提出的不同问题:

1.) 在初始化子类之前将指向基类的指针插入向量是否会防止或导致从该指针检索继承类的问题。[例如切片。]

答:不,只要您 100% 确定所指向的相关类型,此机制不会导致这些问题,但请注意以下几点:

如果派生构造函数失败,稍后您可能会遇到一个问题,因为您可能至少有一个悬空指针位于向量中,因为它[派生类]认为它正在获得的地址空间将被释放到操作环境失败时,但向量的地址仍然是基类类型。

请注意,矢量虽然有点用,但并不是最好的结构,即使是这样,这里也应该涉及一些控制反转,以允许矢量对象控制对象的初始化,以便您了解成功/失败。

这些点导致了隐含的第二个问题:

2.) 这是一个很好的池化模式吗?

答:不是真的,由于上述原因,加上其他原因(将向量推过它的端点基本上会以 malloc 结束,这是不必要的,会影响性能。)理想情况下,您想使用池化库或模板类,甚至更好的是,将分配/取消分配策略实现与池实现分开,已经暗示了一个低级解决方案,即从池初始化中分配足够的池内存,然后使用指针从内部无效池地址空间(请参阅上面的 Alex Zywicki 的解决方案。)使用此模式,池销毁是安全的,因为将是连续内存的池可以在没有任何悬空问题的情况下整体销毁,或由于丢失对对象的所有引用而导致内存泄漏(丢失对存储管理器通过池分配地址的对象的所有引用会留下脏块,但不会导致内存泄漏,因为它由池管理执行。

在 C/C++ 的早期(在 STL 大规模扩散之前),这是一个很好的讨论模式,许多实现和设计可以在很好的文献中找到:例如:

Knuth (1973 The art of computer programming: Multiple volumes),更完整的列表,更多关于池的信息,请参见:

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

第三个隐含的问题似乎是:

3)这是使用池的有效场景吗?

回答:这是一个本地化的设计决策,基于您对什么感到满意,但老实说,您的实现(没有控制结构/聚合,可能循环共享对象子集)向我表明,您最好使用包装器对象的基本链表,每个对象都包含一个指向超类的指针,仅用于寻址目的。您的循环结构建立在此之上,您只需根据需要修改/扩大缩小列表,以根据需要容纳所有第一类对象,完成后,您可以在 O(1) 操作中有效地轻松销毁它们从链表中。

话虽如此,我个人建议此时(当您遇到池确实有用的场景并且您处于正确的思维模式时)进行构建存储管理/池集的类现在是参数化/无类型,因为它将为您提供良好的未来。

于 2014-10-22T15:22:09.083 回答
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;
};

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

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

于 2013-05-04T20:34:24.683 回答
2

这听起来像我听说的线性分配器。我将解释我如何理解它是如何工作的基础知识。

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

不要担心我继承了什么。我一直在将此分配器与内存池结合使用。但基本上不是从 operator 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);
    }
}

此代码功能齐全,除了由于我继承和使用内存池而需要更改的几行。但我敢打赌,您可以弄清楚需要更改的内容,如果您需要手动更改代码,请告诉我。此代码尚未在任何类型的专业庄园中进行过测试,并且不能保证是线程安全的或类似的任何花哨的东西。我只是把它搅起来,想我可以和你分享,因为你似乎需要帮助。

如果您认为它可能对您有所帮助,我也有一个完全通用的内存池的工作实现。如果您需要,我可以解释它是如何工作的。

如果您需要任何帮助,请再次告诉我。祝你好运。

于 2014-01-25T09:51:01.750 回答