Что еще я могу сделать, чтобы улучшить производительность в этом классе? - PullRequest
6 голосов
/ 03 ноября 2011

В настоящее время я занимаюсь разработкой 2D-игры на C # / XNA. Основная особенность игры - пули с совершенно другим поведением (это будет своего рода игра с пулями в аду). Обновление всех маркеров может занять довольно много времени, так как они могут быть бесконечно сложными с их поведением. И все они должны сделать 1 проверку столкновения. Первоначально я просто сохранял их в списке, обновлял и рисовал их все, удаляя неактивные маркеры из списка в каждом кадре. Это, однако, очень быстро замедлило игру, когда на экране было 8k маркеров, поэтому я решил реализовать многопоточность и использовать LINQ для повышения производительности.

Дело в том, что он все еще замедляется на скорости около 16 тыс. Пуль. Мне сказали, что я могу достичь до 7 МИЛЛИОНОВ активных пуль, если я все сделал правильно, поэтому я не удовлетворен 16k ...

Есть ли что-то еще, что я мог бы сделать, чтобы улучшить производительность здесь?

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

internal class BulletManager : GameComponent
{
    public static float CurrentDrawDepth = .82f;
    private readonly List<Bullet> _bullets = new List<Bullet>();
    private readonly int _processorCount;
    private int _counter;
    private readonly Task[] _tasks; 

    public BulletManager(Game game)
        : base(game)
    {
        _processorCount = VariableProvider.ProcessorCount;
        _tasks = new Task[_processorCount];
    }

    public void ClearAllBullets()
    {
        _bullets.Clear();
    }

    public void AddBullet(Bullet bullet)
    {
        _bullets.Add(bullet);
    }

    public override void Update(GameTime gameTime)
    {
        if (StateManager.GameState != GameStates.Ingame &&
            (StateManager.GameState != GameStates.Editor || EngineStates.GameStates != EEngineStates.Running))
            return;

        var bulletCount = _bullets.Count;
        var bulletsToProcess = bulletCount / _processorCount;

        //Split up the bullets to update among all available cores using Tasks and a lambda expression
        for (var i = 0; i < _processorCount; ++i )
        {
            var x = i;
            _tasks[i] = Task.Factory.StartNew( () =>
                                       {
                                           for(var j = bulletsToProcess * x; j < bulletsToProcess * x + bulletsToProcess; ++j)
                                           {
                                               if (_bullets[j].Active)
                                                    _bullets[j].Update();
                                           }
                                       });
        }

        //Update the remaining bullets (if any)
        for (var i = bulletsToProcess * _processorCount; i < bulletCount; ++i)
        {
            if (_bullets[i].Active)
                _bullets[i].Update();
        }
        //Wait for all tasks to finish
        Task.WaitAll(_tasks);

        //This is an attempt to reduce the load per frame, originally _bullets.RemoveAll(s => !s.Active) ran every frame.
        ++_counter;
        if (_counter != 300) return;
        _counter = 0;
        _bullets.RemoveAll(s => !s.Active);
    }

    public void Draw(SpriteBatch spriteBatch)
    {
        if (StateManager.GameState != GameStates.Ingame && StateManager.GameState != GameStates.Editor) return;

        spriteBatch.DrawString(FontProvider.GetFont("Mono14"), _bullets.Count.ToString(), new Vector2(100, 20),
                               Color.White);

        //Using some LINQ to only draw bullets in the viewport
        foreach (var bullet in _bullets.Where(bullet => Camera.ViewPort.Contains(bullet.CircleCollisionCenter.ToPoint())))
        {
            bullet.Draw(spriteBatch);
            CurrentDrawDepth -= .82e-5f;
        }
        CurrentDrawDepth = .82f;
    }
}

Ответы [ 6 ]

7 голосов
/ 04 ноября 2011

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

Измерение производительности. На самом базовом уровне счетчик частоты кадров (или, что еще лучше, счетчик времени кадров). Вы хотите проверить, что вы делаете вещи лучше.

Не выделяйте память во время игрового цикла. Лучший способ проверить, действительно ли вы - это использовать CLR Profiler . Хотя вы, возможно, не используете new (для выделения class типов, structs в порядке), меня не удивит, если большая часть этого LINQ выделяет память за кулисами.

Обратите внимание, что ToString выделит память. Существуют способы выделения номеров (используя StringBuilder), если они вам нужны.

Эта статья дает больше информации.

Не используйте LINQ. LINQ - это простой и удобный и абсолютно не самый быстрый и не самый эффективный способ управления коллекциями.

Используйте подход, основанный на данных. Ключевая идея, лежащая в основе подхода, управляемого данными, заключается в поддержании когерентности кэша ( больше информации ). То есть: все ваши Bullet данные линейно хранятся в памяти. Чтобы сделать это, убедитесь, что Bullet является struct, и вы храните их в List<Bullet>. Это означает, что когда один Bullet загружается в кэш ЦП, он приносит с собой другие (память загружается в кэш большими блоками), сокращая время, которое ЦП тратит на ожидание загрузки памяти.

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

Используйте SpriteBatch для повышения производительности. Сделайте отдельную партию спрайтов (блок Begin()/End()) для ваших пуль. Используйте SpriteSortMode.Deferred - это самый быстрый режим. Делать сортировку (как подразумевается вашим CurrentDrawDepth) медленно! Убедитесь, что все ваши пули используют одну и ту же текстуру (при необходимости используйте атлас текстуры). Помните, что пакетирование - это только повышение производительности, если последовательные спрайты имеют общую текстуру. ( Подробнее )

Если вы используете SpriteBatch, тогда , вероятно, будет быстрее отрисовывать все ваши спрайты, а затем позволить графическому процессору отбирать их, если они не на экране.

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

(ПРИМЕЧАНИЕ. Помимо этого, эти изменения сложно реализовать, они затруднят чтение вашего кода и даже могут сделать его медленнее. Внедряйте их только в случае крайней необходимости и измеряйте производительность.)

(Необязательно) Вставьте свой код в код. Как только вы начнете получать тысячи кодов, вам может потребоваться встроить свой код (удалить вызовы методов), чтобы снизить производительность. Компилятор C # не встроен, а JIT делает это только немного, поэтому вам нужно вставить вручную. Вызовы методов включают в себя такие вещи, как операторы + и *, которые вы можете использовать для векторов - их вставка улучшит производительность.

(Необязательно) Используйте пользовательский шейдер. Если вы хотите еще большей производительности, чем простое использование SpriteBatch, напишите пользовательский шейдер, который берет ваши данные Bullet и рассчитывает как можно больше на GPU .

(Необязательно) Сделайте ваши данные еще меньше и (если возможно) неизменными. Сохраните ваши начальные условия (положение, направление, метку времени) в вашей структуре Bullet. Затем используйте основные уравнения движения , чтобы вычислить ток положение / скорость / и т. Д. Только по мере необходимости. Вы можете часто получать эти расчеты бесплатно, поскольку у вас, вероятно, неиспользуемое время процессора, пока оно ожидает память.

Если ваши данные неизменны, то вы можете избежать передачи их на GPU каждый кадр! (Если вы добавляете / удаляете маркеры, вам придется обновлять их на GPU в этих кадрах).

Если бы вы реализовали все эти элементы, я думаю, вы могли бы получить до 7 миллионов пуль на хорошей машине. Хотя это, вероятно, оставит немного процессорного времени до конца вашей игры.

1 голос
/ 03 ноября 2011

Почему вы удаляете неактивные пули?

Я думаю, что подобные вещи часто решаются с помощью концепции «пула» - возможно, я что-то упускаю из вашего кода, но кажется, что у вас уже есть концепция активного, так зачем удалять неактивное затем создать новую пулю, которая в какой-то момент снова будет удалена для обработки GC. Просто используйте неактивную пулю.

Кроме того, я не могу сказать вам, насколько это больно, но использование ToString () в вашем розыгрыше 30 раз в секунду создает мусор для очистки.

1 голос
/ 03 ноября 2011
  1. Профилируйте его и смотрите, где находятся горячие точки
  2. Я сомневаюсь, что использование задач повышает производительность.На самом деле они могут даже замедлить вашу игру.
  3. Сколько у вас неактивных пуль?Возможно, удаление их в начале игрового цикла немного улучшит производительность.
0 голосов
/ 03 ноября 2011
Профилирование

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

Я бы также попытался реорганизовать вложенные циклы for, чтобы уменьшить количество итераций, как это (непроверенный, в верхней части моего кода кода ниже):

_tasks.ForEach(i=>
{
 i.Factory.StartNew(()=>
 {
  _bullets.Where(j=> _bullets.IndexOf(j)%_tasks.IndexOf(i)==0 && j.Active).Update();
 }
}
);
0 голосов
/ 03 ноября 2011

По крайней мере для пуль за кадром (или за пределами экрана с полем) вы можете заменить целое использование пули, проверив, стреляет ли она по тому, что должно ударить, и отправив отложенное сообщение в цель удара, по которому она попала. пуля в N раз. Затем отложенное сообщение заменяет все вычисления UPDATE этих пуль и по-прежнему наносит ущерб.

0 голосов
/ 03 ноября 2011

Если методы маркеров Update() являются узким местом (убедитесь, что вы делаете то, что предлагает @PiRX, и сначала используйте профилировщик, чтобы найти узкие места), вы можете:

a) Обновляйте только видимые маркеры в каждом кадре и обновляйте невидимые маркеры реже.

b) Упростить процесс обновления: скажем, пуля выполняет свое специфическое (отнимающее много времени) поведение каждые 10 кадров (каждые 0,5 секунды, что угодно) и выполняет некоторую простую вещь (например, полет прямо) в остальное время.

Оба предложения являются компромиссом между производительностью и точностью, конечно.

...