web-dev-qa-db-fra.com

Modèle de conception de pool de mémoire C ++ 11?

J'ai un programme qui contient une phase de traitement qui doit utiliser un tas d'instances d'objets différentes (toutes allouées sur le tas) à partir d'un arbre de types polymorphes, tous finalement dérivés d'une classe de base commune.

Comme les instances peuvent se référencer cycliquement et n'ont pas de propriétaire clair, je veux les allouer avec new, les gérer avec des pointeurs bruts et les laisser en mémoire pour la phase (même si elles ne sont plus référencées) , puis après la phase du programme qui utilise ces instances, je souhaite les supprimer toutes en même temps.

Voici comment j'ai pensé à le structurer:

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

    ...
}

Mis à part le fait de veiller à ce que toutes les instances B soient allouées avec new et que personne n'utilise de pointeurs vers elles après l'effacement du pool de mémoire, y a-t-il des problèmes avec cette implémentation?

Plus précisément, je suis préoccupé par le fait que le pointeur this est utilisé pour construire un std::unique_ptr dans le constructeur de classe de base, avant la fin du constructeur de classe dérivée. Cela entraîne-t-il un comportement indéfini? Si oui, existe-t-il une solution de contournement?

35
Andrew Tomazos

Si vous ne l'avez pas déjà fait, familiarisez-vous avec Boost.Pool . De la documentation Boost:

Qu'est-ce que le pool?

L'allocation de pool est un schéma d'allocation de mémoire qui est très rapide, mais limité dans son utilisation. Pour plus d'informations sur l'allocation de pool (également appelée stockage séparé simple, voir concepts concepts et Stockage séparé simple .

Pourquoi devrais-je utiliser Pool?

L'utilisation des pools vous donne plus de contrôle sur la façon dont la mémoire est utilisée dans votre programme. Par exemple, vous pourriez avoir une situation où vous souhaitez allouer un tas de petits objets à un moment donné, puis atteindre un point dans votre programme où aucun d'entre eux n'est plus nécessaire. À l'aide des interfaces de pool, vous pouvez choisir d'exécuter leurs destructeurs ou simplement les déposer dans l'oubli; l'interface de pool garantira qu'il n'y a pas de fuite de mémoire système.

Quand dois-je utiliser Pool?

Les pools sont généralement utilisés lorsqu'il y a beaucoup d'allocation et de désallocation de petits objets. Une autre utilisation courante est la situation ci-dessus, où de nombreux objets peuvent être supprimés de la mémoire.

En général, utilisez les pools lorsque vous avez besoin d'un moyen plus efficace d'effectuer un contrôle de mémoire inhabituel.

Quel allocateur de pool dois-je utiliser?

pool_allocator est une solution plus générale, conçue pour répondre efficacement aux demandes de n'importe quel nombre de blocs contigus.

fast_pool_allocator est également une solution polyvalente, mais est orientée vers le traitement efficace des demandes pour un bloc à la fois; cela fonctionnera pour des morceaux contigus, mais pas aussi bien que pool_allocator.

Si vous êtes sérieusement préoccupé par les performances, utilisez fast_pool_allocator lorsqu'il s'agit de conteneurs tels que std::list, et utilise pool_allocator lorsqu'il s'agit de conteneurs tels que std::vector.

La gestion de la mémoire est une tâche délicate (threading, mise en cache, alignement, fragmentation, etc., etc.) Pour un code de production sérieux, des bibliothèques bien conçues et soigneusement optimisées sont la voie à suivre, sauf si votre profileur présente un goulot d'étranglement.

15
TemplateRex

Votre idée est géniale et des millions d'applications l'utilisent déjà. Ce modèle est plus connu sous le nom de "pool d'autorelease". Il constitue une base pour une gestion de mémoire "intelligente" dans les frameworks Cocoa et Cocoa Touch Objective-C. Malgré le fait que C++ offre beaucoup d'autres alternatives, je pense toujours que cette idée a beaucoup de sens. Mais il y a peu de choses où je pense que votre mise en œuvre telle qu'elle se présente peut échouer.

Le premier problème auquel je peux penser est la sécurité des threads. Par exemple, que se passe-t-il lorsque des objets de la même base sont créés à partir de différents threads? Une solution pourrait être de protéger l'accès au pool avec des verrous mutuellement exclusifs. Bien que je pense qu'une meilleure façon de le faire est de faire de ce pool un objet spécifique au thread.

Le deuxième problème est d'invoquer un comportement non défini dans le cas où le constructeur de la classe dérivée lève une exception. Vous voyez, si cela se produit, l'objet dérivé ne sera pas construit, mais le constructeur de votre B aurait déjà poussé un pointeur vers this vers le vecteur. Plus tard, lorsque le vecteur est effacé, il tentera d'appeler un destructeur via une table virtuelle de l'objet qui n'existe pas ou qui est en fait un objet différent (parce que new pourrait réutiliser cette adresse).

La troisième chose que je n'aime pas, c'est que vous n'avez qu'un seul pool global, même s'il est spécifique au thread, qui ne permet tout simplement pas un contrôle plus fin sur la portée des objets alloués.

Compte tenu de ce qui précède, je ferais quelques améliorations:

  1. Disposez d'une pile de pools pour un contrôle de portée plus précis.
  2. Faites de cette pile de pool un objet spécifique au thread.
  3. En cas d'échecs (comme une exception dans le constructeur de classe dérivée), assurez-vous que le pool ne contient pas de pointeur suspendu.

Voici ma solution littéralement de 5 minutes, ne jugez pas pour rapide et sale:

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

Hmm, j'avais besoin presque exactement de la même chose récemment (pool de mémoire pour une phase d'un programme qui est effacé d'un coup), sauf que j'avais la contrainte de conception supplémentaire que tous mes objets seraient assez petits.

J'ai trouvé le "pool de mémoire pour petits objets" suivant - peut-être qu'il vous sera utile:

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

Pour répondre à votre question, vous n'invoquez pas un comportement indéfini car personne n'utilise le pointeur jusqu'à ce que l'objet soit entièrement construit (la valeur du pointeur elle-même peut être copiée en toute sécurité jusque-là). Cependant, il s'agit d'une méthode plutôt intrusive, car les objets eux-mêmes doivent connaître le pool de mémoire. De plus, si vous construisez un grand nombre de petits objets, il serait probablement plus rapide d'utiliser un véritable pool de mémoire (comme le fait mon pool) au lieu d'appeler à new pour chaque objet.

Quelle que soit l'approche de type pool que vous utilisez, veillez à ce que les objets ne soient jamais manuellement deleteed, car cela conduirait à un double gratuit!

4
Cameron

Je pense toujours que c'est une question intéressante sans réponse définitive, mais permettez-moi de la décomposer en différentes questions que vous posez réellement:

1.) L'insertion d'un pointeur vers une classe de base dans un vecteur avant l'initialisation d'une sous-classe empêche ou provoque des problèmes avec la récupération des classes héritées de ce pointeur. [découpage par exemple.]

Réponse: Non, tant que vous êtes sûr à 100% du type pertinent pointé, ce mécanisme ne provoque pas ces problèmes, mais notez les points suivants:

Si le constructeur dérivé échoue, vous vous retrouvez avec un problème plus tard lorsque vous êtes susceptible d'avoir un pointeur suspendu au moins assis dans le vecteur, car cet espace d'adressage qu'il [la classe dérivée] pensait qu'il obtiendrait serait libéré dans l'environnement d'exploitation en cas d'échec, mais le vecteur a toujours l'adresse comme étant du type de classe de base.

Notez qu'un vecteur, bien qu'utile, n'est pas la meilleure structure pour cela, et même si c'était le cas, il devrait y avoir une inversion de contrôle impliquée ici pour permettre à l'objet vectoriel de contrôler l'initialisation de vos objets, afin que vous ayez conscience de succès/échec.

Ces points conduisent à la 2ème question implicite:

2.) Est-ce un bon schéma de mise en commun?

Réponse: Pas vraiment, pour les raisons mentionnées ci-dessus, ainsi que d'autres (Pousser un vecteur au-delà de son point final se termine essentiellement par un malloc qui est inutile et aura un impact sur les performances.) Idéalement, vous voulez utiliser une bibliothèque de regroupement ou une classe de modèle, et encore mieux, séparez la mise en œuvre de la politique d'allocation/désallocation de la mise en œuvre du pool, avec une solution de bas niveau déjà suggérée, qui consiste à allouer une mémoire de pool adéquate à l'initialisation du pool, puis à l'utiliser à l'aide de pointeurs pour annuler de l'intérieur l'espace d'adressage du pool (voir la solution d'Alex Zywicki ci-dessus.) En utilisant ce modèle, la destruction du pool est sûre car le pool qui sera la mémoire contiguë peut être détruit en masse sans aucun problème pendant, ou la mémoire fuit en perdant toutes les références à un objet ( perdre toute référence à un objet dont l'adresse est allouée via le pool par le gestionnaire de stockage vous laisse des morceaux sales, mais ne causera pas de fuite de mémoire car il est géré par le pool impl ementation.

Dans les premiers jours du C/C++ (avant la prolifération massive de la STL), c'était un modèle bien discuté et de nombreuses implémentations et conceptions peuvent être trouvées dans la bonne littérature: Par exemple:

Knuth (1973 L'art de la programmation informatique: plusieurs volumes), et pour une liste plus complète, avec plus sur la mise en commun, voir:

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

La troisième question implicite semble être:

3) Est-ce un scénario valide pour utiliser le pooling?

Réponse: Il s'agit d'une décision de conception localisée basée sur ce que vous êtes à l'aise, mais pour être honnête, votre implémentation (pas de structure/agrégat de contrôle, éventuellement partage cyclique de sous-ensembles d'objets) me suggère que vous feriez mieux avec un liste liée basique d'objets wrapper, dont chacun contient un pointeur vers votre superclasse, utilisé uniquement à des fins d'adressage. Vos structures cycliques sont construites en plus de cela, et vous modifiez/agrandissez simplement la liste comme requis pour accueillir tous vos objets de première classe selon les besoins, et une fois terminé, vous pouvez facilement les détruire efficacement dans un O(1) opération à partir de la liste chaînée.

Cela dit, je recommanderais personnellement à ce moment (lorsque vous avez un scénario où la mise en commun a une utilité et que vous êtes donc dans le bon état d'esprit) de réaliser la construction d'un ensemble de classes de gestion/mise en commun du stockage qui sont paramétrés/sans type maintenant car ils vous tiendront en bonne place pour l'avenir.

3
user2654834

Cela ressemble à ce que j'ai entendu appeler un allocateur linéaire. Je vais expliquer les bases de la façon dont je comprends comment cela fonctionne.

  1. Allouez un bloc de mémoire en utilisant :: operator new (size);
  2. Avoir un vide * qui est votre pointeur vers le prochain espace libre en mémoire.
  3. Vous aurez une fonction alloc (size_t size) qui vous donnera un pointeur vers l'emplacement dans le bloc de la première étape pour que vous puissiez continuer à utiliser Placement New
  4. Le placement new ressemble à ... int * i = new (location) int (); où location est un vide * sur un bloc de mémoire que vous avez alloué depuis l'allocateur.
  5. lorsque vous avez terminé avec toute votre mémoire, vous appellerez une fonction Flush () qui désallouera la mémoire du pool ou au moins effacera les données.

J'ai programmé un de ces derniers récemment et je posterai mon code ici pour vous et ferai de mon mieux pour vous l'expliquer.

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

ne vous inquiétez pas de ce dont je hérite. j'utilise cet allocateur avec un pool de mémoire. mais fondamentalement, au lieu d'obtenir la mémoire de l'opérateur nouveau, j'obtiens de la mémoire à partir d'un pool de mémoire. le fonctionnement interne est essentiellement le même.

Voici l'implémentation:

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

Ce code est entièrement fonctionnel à l'exception de quelques lignes qui devront être modifiées en raison de mon héritage et de l'utilisation du pool de mémoire. mais je parie que vous pouvez comprendre ce qui doit changer et faites-moi savoir si vous avez besoin d'une main pour changer le code. Ce code n'a été testé dans aucune sorte de manoir professionnel et n'est pas garanti pour être thread-safe ou quelque chose de fantaisiste comme ça. Je l'ai juste fouetté et j'ai pensé que je pourrais le partager avec vous car vous sembliez avoir besoin d'aide.

J'ai également une implémentation fonctionnelle d'un pool de mémoire entièrement générique si vous pensez que cela peut vous aider. Je peux vous expliquer comment cela fonctionne si vous en avez besoin.

Encore une fois, si vous avez besoin d'aide, faites-le moi savoir. Bonne chance.

2
Alex Zywicki