C++11 memory pool design pattern?

前端 未结 5 417
鱼传尺愫
鱼传尺愫 2020-12-07 13:34

I have a program that contains a processing phase that needs to use a bunch of different object instances (all allocated on the heap) from a tree of polymorphic types, all e

5条回答
  •  盖世英雄少女心
    2020-12-07 14:38

    Hmm, I needed almost exactly the same thing recently (memory pool for one phase of a program that gets cleared all at once), except that I had the additional design constraint that all my objects would be fairly small.

    I came up with the following "small-object memory pool" -- perhaps it will be of use to you:

    #pragma once
    
    #include "defs.h"
    #include       // uintptr_t
    #include       // std::malloc, std::size_t
    #include   // std::alignment_of
    #include       // std::forward
    #include     // std::max
    #include       // 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();
    //     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 static void DtorWrapper(void* obj) { static_cast(obj)->~T(); }
    
    public:
        explicit SmallObjectArena(std::size_t initialPoolSize = 8192)
            : currentBlock(nullptr)
        {
            assert(initialPoolSize >= sizeof(Block) + std::alignment_of::value);
            assert(initialPoolSize >= 128);
    
            createNewBlock(initialPoolSize);
        }
    
        ~SmallObjectArena()
        {
            this->free();
            std::free(currentBlock->rawBlock);
        }
    
        template
        inline T* create()
        {
            return new (alloc()) T();
        }
    
        template
        inline T* create(A1&& a1)
        {
            return new (alloc()) T(std::forward(a1));
        }
    
        template
        inline T* create(A1&& a1, A2&& a2)
        {
            return new (alloc()) T(std::forward(a1), std::forward(a2));
        }
    
        template
        inline T* create(A1&& a1, A2&& a2, A3&& a3)
        {
            return new (alloc()) T(std::forward(a1), std::forward(a2), std::forward(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(currentBlock) + sizeof(Block)) {
                    auto startOfRecord = endOfRecord - sizeof(Record);
                    auto record = reinterpret_cast(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(currentBlock) + sizeof(Block);
        }
    
    private:
        template
        static inline char* alignFor(char* ptr)
        {
            const size_t alignment = std::alignment_of::value;
            return ptr + (alignment - (reinterpret_cast(ptr) % alignment)) % alignment;
        }
    
        template
        T* alloc()
        {
            char* objectLocation = alignFor(currentBlock->startOfNextRecord);
            char* nextRecordStart = alignFor(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(currentBlock->startOfNextRecord);
                nextRecordStart = alignFor(objectLocation + sizeof(T));
            }
            auto record = reinterpret_cast(nextRecordStart);
            record->dtor = &DtorWrapper;
            assert(objectLocation - currentBlock->startOfNextRecord < 32768);
            record->objectOffset = static_cast(objectLocation - currentBlock->startOfNextRecord);
            assert(nextRecordStart - currentBlock->startOfNextRecord < 32768);
            record->endOfPrevRecordOffset = static_cast(nextRecordStart - currentBlock->startOfNextRecord);
            currentBlock->startOfNextRecord = nextRecordStart + sizeof(Record);
    
            return reinterpret_cast(objectLocation);
        }
    
        void createNewBlock(size_t newBlockSize)
        {
            auto raw = static_cast(std::malloc(newBlockSize));
            auto blockStart = alignFor(raw);
            auto newBlock = reinterpret_cast(blockStart);
            newBlock->rawBlock = raw;
            newBlock->prevBlock = currentBlock;
            newBlock->startOfNextRecord = blockStart + sizeof(Block);
            newBlock->size = newBlockSize;
            currentBlock = newBlock;
        }
    
    private:
        Block* currentBlock;
    };
    

    To answer your question, you're not invoking undefined behaviour since nobody is using the pointer until the object is fully constructed (the pointer value itself is safe to copy around until then). However, it's a rather intrusive method, as the object(s) themselves need to know about the memory pool. Additionally, if you're constructing a large number of small objects, it would likely be faster to use an actual pool of memory (like my pool does) instead of calling out to new for every object.

    Whatever pool-like approach you use, be careful that the objects are never manually deleteed, because that would lead to a double free!

提交回复
热议问题