2014-01-18 4 views
0

Я делаю простую систему частиц в C#/XNA, и поскольку в каждую секунду потенциально может быть большой объем вызовов методов, я хотел бы убедиться, что я точно понял, как все работает.Оптимизация с использованием типов значений и вызовов методов с использованием ref

У меня есть частица и класс эмиттер, что:

public sealed class Emitter 
{ 
    private struct Particle 
    { 
     public Vector2 Position; 
     public Vector2 Velocity; 
     public ushort Life; 
     public bool Alive { get { return (Life > 0); } } 
    } 

    private readonly Particle[] _particles; 

    public Emitter(ushort maxParticles) 
    { 
     _particles = new Particle[maxParticles]; 
    } 
} 

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

public static void UpdateParticle(Particle p) 
{ 
    p.Position += p.Velocity; 
} 

Если бы я сделал Emitter с 100000 частиц (как маловероятно, так как это было бы), копирование частицы просто для обновления кажется, что это будет делать много ненужной работы. Если метод использовал ref вместо UpdateParticle(ref Particle p) { ... }, то он просто получил бы доступ к данным напрямую и обновил его там, не так ли?

Однако, по отношению к ответу Джона Скита на this вопрос, он пишет:

Вы почти никогда не нужно использовать реф/из. Это в основном способ получить другое возвращаемое значение и обычно следует избегать именно потому, что это означает, что метод, вероятно, пытается сделать слишком много. Это не всегда так (TryParse и т. Д. Являются каноническими примерами разумного использования out), но использование ref/out должно быть относительной редкостью.

Является ли это одним из тех «относительно редких» случаев, когда это правильный выбор для его использования?

Что касается моего выбора дизайна для этого класса:

  • Я сделал некоторые исследования в системе частиц некоторое время назад. Я не могу вспомнить точные аргументы в пользу создания структуры - что-то о доступе к непрерывному фрагменту памяти быстрее или не так много ссылок на объекты, но бенчмаркинг доказывает, что он работает лучше, чем класс.

  • Particle - частный тип, потому что класс Emitter - это единственное, что когда-либо заботится об этом. Когда-либо. Клиентский код никогда не должен заботиться о отдельных частицах, только то, что Emitter делает блеск и рендеринг.

  • _particles - фиксированный размер, действующий как пул объектов, поскольку повторная утилизация выделенной памяти должна быть более эффективной, чем при вызове new().

ответ

1

Тип вашей частицы будет иметь стоимость памяти не менее 20 байт.

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

public static Particle UpdateParticle(Particle p) 
{ 
    p.Position += p.Velocity; 
    return p; 
} 

Если вы сделаете это, вы добавите копию Particle передать частицы в UpdateParticle() метод, и частицы копировать, чтобы вернуть копию.Поскольку копия представляет собой прочитанную &, для записи копии требуется 40-битный доступ к памяти (а 2 копии - 80 байт).

Обновление 100 000 частиц без использования ref требует копирования около 8 000 000 байтов.

С обычными процессорами Core i5/i7 пропускная способность памяти составляет от 15 до 40 ГБ/с. Накладные расходы на доступ к памяти, не использующие ref, могут быть оценены как 8/20 000 = 0,4 мс. С моим Core 2 с пропускной способностью 6 Гбит/с: 8/6 000 = 1,3 мс)

Для измерения дельты вам нужно иметь дело не менее 10 000 000 частиц (100 раз больше = 130 мс на моем компьютер).

На мой компьютер меры указывают около 600 мс для версии копии и около 200 мс для версии ref.

Я также добавил метод элемента Update() для структуры частиц и обновления 10 000 000 частиц с этим методом также занимает около 200 мс.

Наконец, я попытался выполнить операцию добавления непосредственно в цикле (без вызовов метода). Он занял всего около 150 мс (усиление 50 мс)

Общая дельта 400 мс (чуть меньше второй половины) почти в 3 раза больше, чем одна память с накладными расходами.

Эти дополнительные накладные расходы обусловлены компилятором JIT.

И, наконец, если использование функции ref не рекомендуется в .NET Framework, это очень интересно, если вы используете относительно большие структуры.

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

В C# строгая изоляция ссылочных типов (классов) и типов значений (структур) является проблематичной, когда вы имеете дело с огромным количеством экземпляров: 1) используя классы, с которыми вы сталкиваетесь с длинным временем выделения единичной памяти кучи. 2) использование структуры, с которой вы сталкиваетесь с длительным периодом автоматического копирования.

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