Search code examples
c++c++11memorymemory-managemententity-component-system

Custom heap pre-allocation for entity-component system


I have an OOP entity-component system that currently works like this:

// In the component system
struct Component { virtual void update() = 0; }
struct Entity
{
    bool alive{true};
    vector<unique_ptr<Component>> components;
    void update() { for(const auto& c : components) c->update(); }
}

// In the user application
struct MyComp : Component
{
    void update() override { ... }
}

To create new entities and components, I use C++'s usual new and delete:

// In the component system
struct Manager
{
    vector<unique_ptr<Entity>> entities;
    Entity& createEntity() 
    { 
        auto result(new Entity);
        entities.emplace_back(result);
        return *result;
    }
    template<typename TComp, typename... TArgs>
        TComp& createComponent(Entity& mEntity, TArgs... mArgs)
    {
        auto result(new TComp(forward<TArgs>(mArgs)...));
        mEntity.components.emplace_back(result);
        return result;
    }
    void removeDead() { /* remove all entities with 'alive == false' - 'delete' is called here by the 'unique_ptr' */ }
}

// In the user application
{
    Manager m;
    auto& myEntity(m.createEntity());
    auto& myComp(m.createComponent<MyComp>(myEntity));
    // Do stuff with myEntity and myComp
    m.removeDead();
}

The system works fine, and I like the syntax and flexibility. However, when continuously adding and removing entities and components to the manager, memory allocation/deallocation slows down the application. (I've profiled and determined that the slow down is caused by new and delete).

I've recently read that it's possible to pre-allocate heap memory in C++ - how can that be applied to my situation?


Desired result:

// In the user application
{
    Manager m{1000}; 
    // This manager can hold about 1000 entities with components 
    // (may not be 1000 because of dynamic component size, 
    // since the user can define it's on components, but it's ok for me)

    auto& myEntity(m.createEntity());
    auto& myComp(m.createComponent<MyComp>(myEntity));
    // Do stuff with myEntity and myComp

    m.removeDead(); 
    // No 'delete' is called here! Memory of the 'dead' entities can
    // be reused for new entity creation
}
// Manager goes out of scope: 'delete' is called here  

Solution

  • Using most of answers and Google as references, I implemented some pre-allocation utilities in my SSVUtils library.

    Prealloc.h

    Example:

    using MemUnit = char;
    using MemUnitPtr = MemUnit*;
    using MemSize = decltype(sizeof(MemUnit)); // Should always be 1 byte
    
    class MemBuffer
    {
        Uptr<MemUnit[]> buffer;
        MemRange range;
    
        MemBuffer(MemSize mSize) : ... 
        { 
            // initialize buffer from mSize
        }
    };
    
    class PreAllocatorChunk
    {
        protected:
            MemSize chunkSize;
            MemBuffer buffer;
            std::stack<MemRange> available;
    
        public:
            PreAllocatorChunk(MemSize mChunkSize, unsigned int mChunks) : ...
            {
                // Add "chunks" to to available...
            }
    
            template<typename T, typename... TArgs> T* create(TArgs&&... mArgs)
            {
                // create on first "chunk" using placement new
                auto toUse(available.top().begin); available.pop();
                return new (toUse) T{std::forward<TArgs>(mArgs)...};
            }
    };
    

    More pre-allocation utilities are available:

    • PreAllocatorDynamic: pre-allocates a big buffer, then, when creating an object, splits the buffer in two parts:

      • [buffer start, buffer start + obj size)
      • [buffer start + obj size, buffer end)

      When an object is destroyed, its occupied memory range is set as "available". If during creation of a new object no big enough "chunk" is found, the pre-allocator tries to unify contiguous memory chunks before throwing a runtime exception. This pre-allocator is sometimes faster than new/delete, but it greatly depends on the size of pre-allocated buffer.

    • PreAllocatorStatic<T>: inherited from PreAllocatorChunk. Size of a chunk is equal to sizeof(T). Fastest pre-allocator, less flexible. Almost always faster than new/delete.