Как эффективно обрабатывать многие объекты обновления в C #? - PullRequest
8 голосов
/ 25 февраля 2010

Я занимаюсь разработкой 2D-шутера с использованием C # и XNA. У меня есть класс, который я назову «bullet», и мне нужно обновлять многие из этих экземпляров каждую долю секунды.

Мой первый способ сделать это - создать общий список маркеров и просто удалять и добавлять новые маркеры по мере необходимости. Но при этом GC вступает в игру часто, и у моей игры периодически возникало резкое отставание. (Вырезано много кода, но я просто хотел показать простой фрагмент)

if (triggerButton)
{
    bullets.Add(new bullet());
}
if (bulletDestroyed)
{
    bullets.Remove(bullet);
}

Моя вторая и текущая попытка состоит в том, чтобы иметь отдельный общий набор пуль, к которому я добавляю пули, когда заканчиваю использовать пули, и выскакиваю пули, когда мне нужна новая, если в стеке что-то есть. Если в стеке ничего нет, я добавляю новую пулю в список. кажется, - сократить резкое отставание, но, опять же, иногда возникает некоторое резкое отставание (хотя я не знаю, связано ли это).

if (triggerButton)
{
    if (bulletStack.Count > 0)
    {
        bullet temp = bulletStack.Pop();
        temp.resetPosition();
        bullets.Add(temp);
    }
    else
    {
        bullets.Add(new bullet());
    }
}
if (bulletDestroyed)
{
    bulletStack.Push(bullet);
    bullets.Remove(bullet);
}

Итак, я знаю, что преждевременная оптимизация - корень всего зла, но это была очень заметная неэффективность, которую я мог уловить рано (и это было еще до того, как пришлось беспокоиться о пулях врага, заполняющих экран). Итак, мои вопросы: вызовет ли сбор неиспользуемых объектов в стек сборку мусора? Сохранятся ли ссылки, или объекты все еще уничтожаются? Есть ли лучший способ справиться с обновлением множества различных объектов? Например, я слишком увлекаюсь? Было бы хорошо просто пройтись по списку и найти неиспользованную пулю таким образом?

Ответы [ 6 ]

11 голосов
/ 25 февраля 2010

Здесь много вопросов, и сложно сказать.

Прежде всего, bullet это структура или класс? Если bullet является классом, то каждый раз, когда вы его создаете, а затем выкорчевываете его (позволяете ему выйти из области видимости или устанавливаете его в null), вы собираетесь добавить что-то, что необходимо собрать GC.

Если вы собираетесь делать многие из них и обновлять их каждый кадр, вы можете рассмотреть возможность использования List<bullet> с bullet в качестве структуры и списком, предварительно выделенным (сгенерируйте его с помощью размер достаточно большой, чтобы вместить все ваши пули, поэтому он не воссоздается, как вы называете List.Add). Это очень поможет с давлением ГХ.

Кроме того, только потому, что мне нужно разглагольствовать:

Итак, я знаю, что преждевременная оптимизация - корень всего зла, но это была очень заметная неэффективность

Никогда, никогда не бойтесь оптимизировать процедуру, которая, как вы знаете, вызывает проблемы . Если вы видите проблему с производительностью (т.е. ваши лаги), это уже не преждевременная оптимизация. Да, вы не хотите оптимизировать каждую строку кода, но вам нужно оптимизировать код , особенно когда вы видите реальную проблему с производительностью. Оптимизировать его, как только вы увидите, что это проблема, гораздо проще, чем пытаться оптимизировать ее позже, поскольку любые требуемые изменения дизайна будут гораздо легче реализовать, прежде чем вы добавите много другого кода, использующего ваш класс bullet.

4 голосов
/ 25 февраля 2010

Вам может пригодиться шаблон дизайна мухи . Должен быть только один объект пули, но несколько весов могут указывать для него разные позиции и скорости. Весы могут быть сохранены в предварительно выделенном массиве (скажем, 100) и помечены как активные или нет.

Это должно полностью исключить сборку мусора и может уменьшить пространство, необходимое для отслеживания податливых свойств каждой пули.

2 голосов
/ 26 февраля 2010

Ваше решение, основанное на стеке, довольно близко к классу, который я написал, чтобы обобщенно выполнить такой пул ресурсов:
http://codecube.net/2010/01/xna-resource-pool/

Вы упомянули, что это в основном устраняет проблему, но она все еще возникает тут и там. Происходит то, что с этим методом пула на основе стека / очереди система достигнет точки стабильности, когда вы больше не будете запрашивать больше новых объектов, чем может предоставить пул. Но если количество запросов превысит ваш предыдущий максимум запрошенных элементов, вам придется создать новый экземпляр для обслуживания запроса (таким образом, время от времени вызывая GC).

Один из способов обойти это - пройти и предварительно выделить столько экземпляров, сколько, по вашему мнению, вам может понадобиться на пике. Таким образом, у вас не будет никаких новых выделений (по крайней мере, из объединенных объектов), и GC не будет срабатывать: -)

2 голосов
/ 25 февраля 2010

Вы правы, полагая, что хранение неиспользованных пуль в стопке предотвращает их сбор мусора.

Что касается причины отставания, пробовали ли вы какие-либо инструменты профилирования? Просто чтобы найти, где проблема.

2 голосов
/ 25 февраля 2010

Я признаю, что у меня нет такого опыта как такового, но я бы подумал об использовании традиционного массива. Инициализируйте массив до размера, который больше необходимого, и будет теоретическим максимальным числом маркеров, скажем, 100. Затем, начиная с 0, присвойте маркеры в начале массива, оставив последний элемент как ноль. Итак, если бы у вас было четыре активных пули, ваш массив выглядел бы так:

0 Б 1 Б 2 Б 3 Б 4 ноль ... 99 ноль

Преимущество состоит в том, что массив всегда будет выделяться, и, следовательно, вы не будете иметь дело с накладными расходами более сложной структуры данных. На самом деле это довольно похоже на работу строк, поскольку они на самом деле char [] с нулевым терминатором.

Может быть стоит выстрел. Один недостаток, вам придется сделать некоторые ручные манипуляции при удалении пули, возможно, переместить все после этой пули вверх в слот. Но в этот момент вы просто перемещаете указатели, так что я не думаю, что это будет иметь большое наказание, как выделение памяти или сборщик мусора.

1 голос
/ 25 февраля 2010

Список на самом деле имеет встроенную емкость для предотвращения выделения для каждого добавления / удаления. Как только вы превышаете емкость, это добавляет больше (я думаю, что я удваиваюсь каждый раз). Проблема может быть скорее в удалении, чем в добавлении. Добавить будет просто на первом открытом месте, которое отслеживается по размеру. Чтобы удалить, список должен быть сжат, чтобы заполнить теперь пустой слот. Если вы всегда удаляете переднюю часть списка, то каждый элемент должен скользить вниз.

Стек все еще использует массив в качестве внутреннего механизма хранения. Таким образом, вы все еще связаны со свойствами добавления / удаления массива.

Чтобы массив работал, вам нужно создать все маркеры с активным свойством для каждого. Когда вам нужен новый, заполните флаг Active на true и установите все новые свойства маркеров. По завершении установите активный флаг false.

Если вы хотите попытаться исключить необходимость повторения списка (который может быть очень большим в зависимости от того, что вы собираетесь разрешить) для каждого перерисовывания, вы можете попытаться реализовать двойной связанный список в массиве. Когда требуется новая пуля, запросите массив для первой доступной бесплатной записи. Перейдите к последнему активному маркеру (переменная) и добавьте новую позицию массива маркера в его следующее свойство активного маркера. Когда пришло время удалить его, перейдите к предыдущему маркеру и измените его свойство активного маркера на удаленный следующий активный.

//I am using public fields for demonstration.  You will want to make them properties
public class Bullet {
  public bool Active;
  public int thisPosition;
  public int PrevBullet = -1;
  public int NextBullet = -1;
  public List<Bullet> list;

  public void Activate(Bullet lastBullet) {
    this.Active = true;
    this.PrevBullet = lastBullet.thisPosition;
    list[this.PrevBullet].NextBullet = this.thisPosition;
  }

  public void Deactivate() {
    this.Active = false;
    list[PrevBullet].NextBullet = this.NextBullet;
    list[NextBullet].PrevBullet= this.PrevBullet;
  }
}

Таким образом, у вас есть готовый массив со всеми необходимыми маркерами, но краска попадает только в активные маркеры, независимо от их положения в массиве. Вам просто нужно сохранить ссылку на первый активный маркер, чтобы начать рисовать, и последний активный маркер, чтобы знать, где список начинается заново.

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

...