Свяжая Кровь GameDevСтатьи

Универсальный менеджер памяти.

Автор:

Вступление
В процессе игры создание и уничтожение объектов происходит довольно часто.
И если для каждого объекта выделять память в хипе(куче), а затем её возвращать
системе, то во-первых эти операции будут отъедать немалую часть процессорного
времени, во-вторых образуется сильная фрагментация памяти, и выделение памяти
для каждого следующего объекта будет ещё дороже.

Выход очевиден - менеджер памяти.

Методы
Для начала перечислю известные мне методы управления памятью:
1) Самый простой и доступный - списки свободной памяти.
2) Пулы.
3) Алгоритм Пекаря и прочая экзотика со сборкой мусора и перемещением объектов
в памяти.

Позвольте сразу уволить вариант №3. Так как это непозволительная роскошь тратить
процессорное время на перемещение объектов в памяти, или, тем паче, жертвовать
половиной выделенной памяти (алгоритм пекаря требует такой жертвы).

Списки памяти оч.эффективны, но всё же если объекты необходимого размера ещё не
удалялись, то нам таки придётся потратить драгоценное время процессора. А если
их сто? тысяча?

Решение здесь публикуемое основывается на коктеле из списка и пула.

Интерфейс
Начну с конца. Что должен делать менеджер памяти в целом? Выделять память и
соответсвенно освобождать её. Ещё не плохо бы добавтиь метод, вызывающий полную
зачистку. итак:

class cMemoryMgr
{
public:
  cMemoryMgr(void) {}
  ~cMemoryMgr(void);

  void*  allocate(size_t); // вызов этого метода помещается в new
  void deallocate(void*, size_t); // а этого в delete

  void cleanup(void); // Тетя Ася :) или Мистер Проппер ;)
};

Ещё было бы странно если менеджер памяти, вдруг, стало возможным копировать.
Так защитмся же от такого срама:

memmgr = mmgrOLD;

А защищаться мы будем так:

class cMemoryMgr
{
private:
  // copy must die :)
  // эти два метода мы оставим без реализации
  // ибо нехай копировать менеджер памяти
  cMemoryMgr(const cMemoryMgr&); 
  cMemoryMgr& operator = (const cMemoryMgr&); 
public:
  cMemoryMgr(void) {}
  ~cMemoryMgr(void);

  void*  allocate(size_t); // вызов этого метода помещается в new
  void deallocate(void*, size_t); // а этого в delete

  void cleanup(void); // С Мистер Проппер веселей, память чище в два раза быстрей! ;)
};

Реализация
Итак. Теперь пришло время обсудить как это будет работать.
Так как я обещал гибрид, вот вам:
1) Мап свободной памяти (size_t -> очередь<freeCell>).
2) Список пулов
-----------------------------------------------------
процесс удаления
1) берём список свободных ячеек с нужным размером,
и, кто бы мог подумать :), добавляем в него ячейку.
-----------------------------------------------------
процесс выделения
1) ищем в карте нужный размер.
  1) берём 1-ую попавшуюся ячейку. радуемся.
2) нам не повезло - ещё никто ничё не удалил.
  1) смотрим есть ли в текущем пуле место для нас?
    1) если есть отхапыаем память. радуемся.
  2) мест нет. забиваем на тот пул. делаем себе новый.
    1) здесь места под солнцем хватает. радуемся.

Думаю надо наконец приняться за написание:

class cMemoryMgr
{
  // класс пула, блока памяти в терминах нашего менеджера
  class MemBlock
  {
  public:
    enum { defsize = 128*1024*1024 }; // размер по-умолчанию
  private:
    uchar* m_block; // соб-сно память :), вернее указатель на неё
    size_t m_remain; // сколько её ещё осталось
    size_t m_size; // сколько её всего
    size_t m_curr; // сколько мы прихватизировали
  private:
    // нехай копировать!
    MemBlock(const MemBlock& cp);
    MemBlock& operator = (const MemBlock& cp);
  public:
    // новый блок с дефолтовым размером
    MemBlock(void);
    // блок с заданным размером, только если
    // заданный больше дефолтового
    MemBlock(size_t);
    ~MemBlock(void);

    //0 == not enough memory
    void* allocate(size_t); // выделить память в этом пуле

    const size_t size(void) const;
    const size_t remaining(void) const;
  };
  // свободная ячейка
  struct MemEntry
  {
    size_t size; // сикоку
    void* ptr; // гиде
    //easy constructor
    MemEntry(void* sp, size_t sz): size(sz), ptr(sp) {}
  };
private:
  // список блоков
  std::vector<cMemoryMgr::MemBlock*> m_blocks;
  // Мап свободной памяти (size_t -> очередь<freeCell>).
  std::map<size_t, std::queue<cMemoryMgr::MemEntry> > m_freeEntrys;
private:
  void addBlock(size_t);
  //copy must die :)
  cMemoryMgr(const cMemoryMgr&);
  cMemoryMgr& operator = (const cMemoryMgr&);
public:
  cMemoryMgr(void) {}
  ~cMemoryMgr(void);

  void*  allocate(size_t);
  void deallocate(void*, size_t);

  void cleanup(void);
};

Вот и получился у нас .h
Мы уже обсуждали как оно работает, так что теперь цпп:

// в коде нет ничего сверхъестественного так что без комментариев
// Не пытайтесь это компилировать. т.к. sal моя либа.
// имемори мгр её часть. если интересно велком на мой сайт
// там я выложу её целиком. линк в конце статьи.
#include "memmgr.h"

using namespace std;
using namespace dreamer;
using namespace SAL;

inline cMemoryMgr::MemBlock::MemBlock(void): m_size(defsize), m_remain(m_size), m_curr(0)
{
  m_block = new uchar[m_size];
  debug_assert(m_block, "MemBlock::MemBlock()", "block is NULL");
  m_remain = m_size;
}

inline cMemoryMgr::MemBlock::MemBlock(size_t theSize): m_size(defsize), m_block(0), m_curr(0)
{
  debug_assert(theSize, "MemBlock::MemBlock(size_t theSize)", "theSize==0");
  if(theSize > m_size) m_size = theSize;
  m_block = new uchar[m_size];
  debug_assert(m_block, "MemBlock::MemBlock()", "block is NULL");
  m_remain = m_size;
}

cMemoryMgr::MemBlock::~MemBlock(void)
{
  debug_assert(m_block, "MemBlock::~MemBlock()", "block is NULL");
  delete[] m_block;
}

inline const size_t cMemoryMgr::MemBlock::size(void) const
{
  return m_size;
}

inline const size_t cMemoryMgr::MemBlock::remaining(void) const
{
  return m_remain;
}

inline void* cMemoryMgr::MemBlock::allocate(size_t bytes)
{
  //it's not a joke. cMemoryMgr must create MemBlock with matching size
  debug_assert(bytes <= m_size, "MemBlock::allocate()", "bytes > block_size");
  if ((m_block == NULL) || (bytes > m_remain))
    return 0;
  void *memory = m_block + m_curr;
  m_curr += bytes;
  m_remain -= bytes;
  return memory;
}

//cMemoryMgr
inline void cMemoryMgr::addBlock(size_t blockSize)
{
  if(blockSize > cMemoryMgr::MemBlock::defsize)
    m_blocks.push_back(new cMemoryMgr::MemBlock(blockSize));
  else m_blocks.push_back(new cMemoryMgr::MemBlock());
}

void* cMemoryMgr::allocate(size_t bytes)
{
  void *memory = 0;
  try
  {
    //try to find free cell
    queue<cMemoryMgr::MemEntry> que = m_freeEntrys[bytes];
    if(!que.empty())
    {
      memory = que.front().ptr;
      que.pop();
    }
    if(!memory)
    {
      if(m_blocks.empty()) addBlock(bytes);
      if((*(m_blocks.end()-1))->remaining() < bytes) 
        addBlock(bytes);
      memory = (*(m_blocks.end()-1))->allocate(bytes);
    }
  }
  dream_catcher("cMemoryMgr::allocate()")

  debug_assert(memory, "cMemoryMgr::allocate()", "memory == 0 even after allocation");
  return memory;
}

void cMemoryMgr::deallocate(void* space, size_t bytes)
{
  m_freeEntrys[bytes].push(cMemoryMgr::MemEntry(space, bytes));
}

void cMemoryMgr::cleanup(void)
{
  map<size_t, std::queue<cMemoryMgr::MemEntry> >::iterator mit = m_freeEntrys.begin();
  for(; mit!=m_freeEntrys.end(); ++mit)
  {
    while(!(mit->second.empty()))
      mit->second.pop();
  }

  vector<cMemoryMgr::MemBlock*>::iterator vit = m_blocks.begin();
  for(; vit!=m_blocks.end(); ++vit)
  {
    delete (*vit);
  }
  m_blocks.clear();
}

cMemoryMgr::~cMemoryMgr(void)
{
  cleanup();
}

Использование
Уфф. Вот уже у нас есть неплохой менеджер памяти (ИМХО).
Пора бы его и в ход пустить.

class Managed
{
  ...
  static cMemoryMgr memMgr;
public:
  ...
  void* operator new (size_t sz)
  {
    return memMgr.allocate(sz);
  }
  void operator delete (void* space, size_t sz)
  {
    memMgr.deallocate(space, sz);
  }
  // ну и все остальные версии
  virtual ~Managed(void) {...}
  ...
};

// в cpp!!!!!!
cMemoryMgr Managed::memMgr;

Затем, по идее, достаточно унаследовать от этого класса и радоваться.

Заключение
Плюсы:
1)Первое, что стоит отметиь это один мощный импрувмент:
Мона расширить решение дебаг-функционалом, чтобы он
расписывал кто куда и сколько, кто вернул, а кто нет.
2)Это быстрое решение во всех смылах: быстро пишется и быстро работает.
3)Мы имеем универсальный интерфейс, так что если приспичит, то
возможно безболезненно поменять внутренности.
Минусы
1) Не слишком экономно по-отношения к памяти:
Если, например, мы хотим выделить объект в 20кб, а осталось 15,
то будет создан новый пул, а эти 15 коту под хвост.
Но здесь, опять же, есть куча импрувментов. Самый простой из них
- это занести эу пятнашку в список св. памяти.

Возможно вы задаётесь вопросом: "А где же подсчёт ссылок?".
Отвечаю.
Его можно засунуть:
1) прям в Managed.
2) в ведущий указатель.
ещё мона прикрутить ведущий/невидимый указатель поверх любого
из пунктов. Этот указатель при копировании сам вызывает Grab(),
а при выходе из области видимости - Release()
Про подсчёт ссылок написано много чего, в том числе и на этом сайте.
Про ведущие и невидимые указатели мона прочитать в "C++ for real programmers"
Дж.Элджер. Собс-но моя статья основывается на мыслях из его книги.
Если кому сильно интересно, но нет возможности почитать оный труд, то
оставте пару строчек в комментариях, возможно будет ещё статья про фсякие
наркоманские указатели.

Вот вроде и всё. Надеюсь здесь нет ляпов.
Обещаный линк Dream->SAL->Source

4 августа 2007 (Обновление: 2 янв 2008)

Комментарии [17]