Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Memory managing container design issue - items require inheritance

I'm designing a memory managing container, with performance and ease of use in mind, especially for game development projects. Here's it in it's current state.

I'll extract the most important parts from the source.

// Uptr is a typedef for std::unique_ptr

class MemoryManageable {
    bool alive{true};
    public: bool isAlive() const { return alive; }
};

template<typename T> struct Deleter {
    bool operator()(const Uptr<T>& mItem) const { return !mItem->isAlive(); } 
};  

template<typename T> class MemoryManager {
    // T is the type of items being stored and must inherit MemoryManageable
    std::vector<Uptr<T>> items; 
    std::vector<T*> toAdd; // will be added to items on the next refresh() call
    Deleter<T> deleter;

    void refresh() { 
        items.erase(std::remove_if(std::begin(items), std::end(items), deleter), std::end(items)); 
        for(const auto& i : toAdd) items.push_back(Uptr<T>(i)); toAdd.clear(); 
    }
    void clear() { items.clear(); toAdd.clear(); }

    // Del sets alive to false, so that the item will be deleted and deallocated on the next refresh() call
    void del(T& mItem) { mItem.alive = false; }

    template<typename TType, typename... TArgs> TType& create(TArgs&&... mArgs) { /* creates a new TType* (derived from T) and puts it in toAdd */ }
    template<typename... TArgs> T& create(TArgs&&... mArgs) { return create<T, TArgs...>(std::forward<TArgs>(mArgs)...); }
}

You can see a real usage here.

The desired usage is something like this:

struct Entity : public MemoryManageable { 
     Manager& manager; 
     void destroy() { manager.del(*this); } 
     ... 
}

struct Mnnager { 
    MemoryManager<Entity> mm; 
    void del(Entity& mEntity) { mm.del(mEntity); }
    ... 
 }

Manager::update() {
    mm.refresh(); // entities with 'alive == false' are deallocated, and entities in 'mm.toAdd' are added to 'mm.items' 
    for(auto& entity : mm) entity->update(); // entities 'die' here, setting their 'alive' to false 
}

This kind of delayed insertion design with refresh() has some great advantages:

  • It's fast
  • An entity can be "killed" even if already dead
  • Entities can be created from other entities as they do not get directly stored in items until populate() is called

However, I would love if there was no need of inheriting MemoryManageable, and if there was a more elegant way to delete entities.

  • Is there a way to make MemoryManager handle the alive bool internally, without having to inherit MemoryManageable, and, most importantly, without any performance overhead?
  • Is there a more elegant way that could be used to delete items handled by MemoryManager?

Ideally, items handled by the MemoryManager should know nothing about it.


Example usage: in gamedev, it's common that an entity gets destroyed during its update. Consider a "Enemy" entity with a int life member: if(life <= 0) this->destroy(); - that would happen easily during an update loop, and if the entity, on destruction, is immediately removed from the Manager, it causes trouble with looping and other entities that point to the dead entity.

like image 243
Vittorio Romeo Avatar asked Jul 09 '13 00:07

Vittorio Romeo


People also ask

What happens if you exceed the memory limit of a container?

Exceed a Container’s memory limit. A Container can exceed its memory request if the Node has memory available. But a Container is not allowed to use more than its memory limit. If a Container allocates more memory than its limit, the Container becomes a candidate for termination.

How much memory does a container need to allocate?

Here is the configuration file for a Pod that has one Container with a memory request of 50 MiB and a memory limit of 100 MiB: In the args section of the configuration file, you can see that the Container will attempt to allocate 250 MiB of memory, which is well above the 100 MiB limit.

What happens when an application uses more memory than the container?

In general, when an application uses more memory than the container memory, the application terminates. The following sample application pushes records at an interval of 10 milliseconds. This fast interval makes the heap grow without bounds, simulating a memory leak.

What are the two types of resource requirements in Docker containers?

You can currently specify two types of requirement, requests and limits, for two types of container resource: memory and CPU. This post aims to explain what these two types of requirement mean and the meaning of memory within the Docker container runtime.


1 Answers

First thing to say: I'm not very fond of C++11, so there could be some syntax error in the code I wrote, but it's the logic that matters.

If I understood your question, you just want to be able to asynchronously add and delete items from a container, without those knowing of their state. In this scenario you can use a std::map< std::unique_ptr< Elem >, bool > to handle the state of items: true = alive, false = dead.

MemoryManager class

fields:

  • std::vector< T * > m_toAdd, a vector with not yet added items;
  • std::map< std::unique_ptr< T >, bool > m_items, a map with every item managed and a bool flag

methods:

  • add(), which adds a new item in the m_toAdd vector;
  • del(), which marks an item to be removed in m_items using its flag;
  • refresh(), which removes dead items and commits m_toAdd items ad alive in m_items, then clears the vector

Elem class

fields:

  • MemoryManager & m_manager, a reference to its memory manager;

methods:

  • Elem(), ctor, which calls m_manager::add();
  • del(), which calls m_manager::del().

Creation

When an Elem is created, it automatically add itself to its memory manager, which adds it to its m_toAdd vector, then when everything is refreshed, those Elems in this vector are passed in the std::map paired with a true boolean (initially marked as alive).

Deletion

When an Elem is to be deleted, it calls a del() method of its manager which simply marks it as dead in its std::map, then when everything is refreshed, every Elem in the manager flagged as dead is removed, and the m_toAdd vector is cleared.

(Also I reccomend you using std::enable_shared_from_this in order to better handle the ptr, but you'll have to use std::shared_ptr, not std::unique_ptr, but that could not be so bad.)

My suggestion is something like:

template< class T >
class MemoryManager
{
   typedef std::unique_ptr< T > unique_t;
   typedef std::map< unique_t, bool > map_t;
   typedef std::pair< unique_t, bool > pair_t;
   typedef std::vector< T * > vector_t;

public:
   void add(T * item)
   {
      m_toAdd.push_back(item);
   }

   void remove(T & item)
   {
      typename map_t::iterator it = m_items.find(item);
      if (it != m_items.end() )(* it).second = false;
   }

   void refresh()
   {
      // clear dead
      typename map_t::iterator it = m_items.begin();
      while ((it = std::find_if(it, m_items.end(), isDead)) != m_items.end())
      {
         m_items.erase(it++);
      }

      // add new
      for(T & item : m_toAdd)
      {
         m_items.insert(std::make_pair(unique_t(item), true));
      }
      m_toAdd.clear();
   }

   void clear()
   {
      m_items.clear();
      m_toAdd.clear();
   }

protected:
   bool isDead(pair_t itemPair)
   {
      // true = alive, false = dead
      return itemPair.second;
   }

private:
   map_t m_items;
   vector_t m_toAdd;
};

class Entity
{
public:
   Entity(MemoryManager< Entity > & manager)
      : m_manager(manager)
   {
      m_manager.add(this);
   }

   void die()
   {
      m_manager.remove(this);
   }

private:
   MemoryManager< Entity > & m_manager;
}; 

NOTE: the code is not tested and certainly broken, the important thing is the logic!

like image 135
Enoah Netzach Avatar answered Oct 31 '22 17:10

Enoah Netzach