Как обработать много объектов обновления эффективно в C#?

Я разрабатываю 2D служебную игру стрелка с помощью C# и XNA. У меня есть класс, что я назову "маркер" и потребность обновить многие из этих экземпляров каждая часть секунды.

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

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);
}

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

8
задан Bob 25 February 2010 в 17:21
поделиться

6 ответов

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

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

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

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

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

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

11
ответ дан 5 December 2019 в 07:34
поделиться

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

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

4
ответ дан 5 December 2019 в 07:34
поделиться

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

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

2
ответ дан 5 December 2019 в 07:34
поделиться

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

0 B 1 B 2 B 3 B 4 null {{1} } ... 99 null

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

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

2
ответ дан 5 December 2019 в 07:34
поделиться

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

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

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

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

//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;
  }
}

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

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

1
ответ дан 5 December 2019 в 07:34
поделиться

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

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

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

2
ответ дан 5 December 2019 в 07:34
поделиться
Другие вопросы по тегам:

Похожие вопросы: