Применима ли рекомендация Microsoft к использованию свойств C # для разработки игр?

26

Я понимаю, что иногда вам нужны свойства, такие как:

public int[] Transitions { get; set; }

или:

[SerializeField] private int[] m_Transitions;
public int[] Transitions { get { return m_Transitions; } set { m_Transitions = value; } }

Но у меня сложилось впечатление, что в разработке игр, если у вас нет причин поступать иначе, все должно быть как можно быстрее. Из того, что я прочитал, у меня сложилось впечатление, что Unity 3D не обязательно обеспечивает встроенный доступ через свойства, поэтому использование свойства приведет к дополнительному вызову функции.

Прав ли я постоянно игнорировать предложения Visual Studio не использовать открытые поля? (Обратите внимание, что мы используем int Foo { get; private set; }там, где есть преимущество в показе предполагаемого использования.)

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

piojo
источник
34
Всегда проверяйте с помощью профилировщика, прежде чем верить, что что-то «медленное». Мне сказали, что что-то «медленно», и профилировщик сказал мне, что он должен выполнить 500 000 000 раз, чтобы потерять полсекунды. Поскольку он никогда не будет выполняться более нескольких сотен раз в кадре, я решил, что это не имеет значения.
Almo
5
Я обнаружил, что немного абстракции здесь и там помогает более широко применять низкоуровневые оптимизации. Например, я видел множество простых проектов на C, использующих различные виды связанных списков, которые ужасно медленны по сравнению с непрерывными массивами (например, для поддержки ArrayList). Это связано с тем, что у C нет универсальных шаблонов, и эти программисты на C, вероятно, обнаружили, что легче повторно реализовать связанные списки, чем сделать то же самое для ArrayListsалгоритма изменения размера. Если вам нужна хорошая низкоуровневая оптимизация, заключите ее в капсулу!
hegel5000
3
@ Almo Применяете ли вы ту же логику к операциям, которые происходят в 10000 разных местах? Потому что это доступ к классу. Логично, что вы должны умножить стоимость производительности на 10 КБ, прежде чем решить, что класс оптимизации не имеет значения. И 0,5 мс - лучшее практическое правило, когда производительность вашей игры будет разрушена, а не полсекунды. Ваша точка зрения остается в силе, хотя я не думаю, что правильное действие так ясно, как вы его себе представляете.
Пихо
5
У вас есть 2 лошади. Гони их.
Мачта
@piojo Я не знаю. Некоторые мысли всегда должны идти в эти вещи. В моем случае это было для циклов в коде пользовательского интерфейса. Один экземпляр пользовательского интерфейса, несколько циклов, несколько итераций. Для записи я проголосовал за ваш комментарий. :)
Almo

Ответы:

44

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

Не обязательно. Как и в прикладном программном обеспечении, в игре есть код, который критичен к производительности, и код, которого нет.

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

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

Я читал, что Unity 3D не уверен, что встроенный доступ через свойства, поэтому использование свойства приведет к дополнительному вызову функции.

С тривиальными свойствами в стиле int Foo { get; private set; }вы можете быть уверены, что компилятор оптимизирует оболочку свойств. Но:

  • если у вас действительно есть реализация, то вы больше не можете быть в этом уверены. Чем сложнее эта реализация, тем меньше вероятность того, что компилятор сможет ее встроить.
  • Свойства могут скрыть сложность. Это обоюдоострый меч. С одной стороны, это делает ваш код более читабельным и изменяемым. Но с другой стороны, другой разработчик, который получает доступ к свойству вашего класса, может подумать, что он просто обращается к одной переменной и не понимает, сколько кода вы на самом деле скрыли за этим свойством. Когда свойство начинает становиться вычислительно дорогой, то я , как правило, реорганизовать его в явном GetFoo()/ SetFoo(value)метод: Для того, чтобы подразумевать , что есть намного больше , что происходит за кулисами. (или если мне нужно быть еще более уверенным, что другие поймут намек: CalculateFoo()/ SwitchFoo(value))
Philipp
источник
Доступ к полю в общедоступном поле не только для чтения типа структуры быстрый, даже если это поле находится внутри структуры, которая находится внутри структуры и т. Д., При условии, что объект верхнего уровня является классом не только для чтения. поле или переменная, не предназначенная только для чтения. Структуры могут быть довольно большими без ущерба для производительности, если эти условия применяются. Нарушение этого паттерна приведет к замедлению, пропорциональному размеру конструкции.
суперкат
5
«Затем я склонен реорганизовать его в явный метод GetFoo () / SetFoo (value):» - Хороший вопрос, я склонен переносить тяжелые операции из свойств в методы.
Марк Роджерс
Есть ли способ подтвердить, что компилятор Unity 3D выполняет оптимизацию, о которой вы упомянули (в сборках IL2CPP и Mono для Android)?
Пихо
9
@piojo Как и в большинстве вопросов о производительности, самый простой ответ - «просто сделай это». У вас есть готовый код - проверьте разницу между наличием поля и наличием свойства. Детали реализации заслуживают изучения только тогда, когда вы действительно можете измерить важное различие между двумя подходами. По моему опыту, если вы пишете все так быстро, насколько это возможно, вы ограничиваете доступные высокоуровневые оптимизации и просто пытаетесь состязаться с низкоуровневыми частями («не вижу леса за деревьями»). Например, поиск способов пропустить Updateполностью сэкономит вам больше времени, чем Updateбыстрое.
Луан
9

В случае сомнений используйте лучшие практики.

Вы сомневаетесь


Это был простой ответ. Конечно, реальность сложнее.

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

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

В-третьих, преимущество свойств и методов доступа в том, что они устойчивы к изменениям и, следовательно, облегчают поддержку и изменение кода - это то, что вам нужно для написания быстрых программ. Хотите создать исключение всякий раз, когда кто-то хочет установить ваше значение равным нулю? Используйте сеттер. Хотите отправить уведомление, когда значение меняется? Используйте сеттер. Хотите записать новое значение? Используйте сеттер. Хотите сделать ленивую инициализацию? Используйте геттер.

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

Питер - Унбан Роберт Харви
источник
2

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

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

Основная причина, по которой я использовал свойства в свои 10 лет в качестве профессионального программиста на C #, заключалась в том, чтобы другие программисты не говорили мне, что я не придерживаюсь стандарта кодирования. Это был хороший компромисс, поскольку вряд ли когда-либо имелись веские основания не использовать собственность.

Ян Рингроз
источник
2

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

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


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

В этом случае у нас есть пара:

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

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

Я говорил код клиента. Это означает код, который вызывает ваш. Если вы не создаете библиотеку (или даже делаете библиотеку, но используете внутреннюю, а не публичную), вы обычно можете сойти с рук и с какой-то хорошей дисциплиной. В этой ситуации лучше всего использовать свойства, чтобы не позволить себе выстрелить себе в ногу. Более того, гораздо проще рассуждать о коде, если вы видите все места, где поле может измениться в одном файле, вместо того, чтобы беспокоиться о том, что оно модифицируется или нет, где-то еще. Фактически, свойства также хороши для установки точек останова, когда вы выясняете, что пошло не так.


Да, есть ценность видеть, что делается в промышленности. Тем не менее, у вас есть мотивация идти против лучших практик? или вы просто идете против лучших практик - усложняете работу кода - только потому, что это сделал кто-то другой? Ах, кстати, «другие делают это» - это то, как вы начинаете культ груза.


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

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


Наконец, вы зарабатываете что-то, идя против лучших практик?

Для конкретных случаев (Unity и Mono для Android), Unity принимает значения по ссылке? Если этого не произойдет, он все равно скопирует значения, без увеличения производительности.

Если это так, если вы передаете эти данные в API, который принимает ref. Имеет ли смысл сделать поле общедоступным, или вы могли бы сделать тип способным напрямую вызывать API?

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

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

Theraot
источник
1

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

:

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

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

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

« Настоящая проблема заключается в том, что программисты потратили слишком много времени, беспокоясь об эффективности в неправильных местах и ​​в неподходящее время ; преждевременная оптимизация - корень всего зла (или, по крайней мере, большей его части) в программировании». Дональд Кнут, 1974.

Винсент О'Салливан
источник
1

Для базовых вещей, подобных этому, оптимизатор C # все равно все сделает правильно. Если вы беспокоитесь об этом (и если вы беспокоитесь об этом для более чем 1% кода, вы делаете это неправильно ), атрибут был создан для вас. Атрибут [MethodImpl(MethodImplOptions.AggressiveInlining)]значительно увеличивает вероятность того, что метод будет встроен (методы получения и установки свойств являются методами).

Джошуа
источник
0

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

Структура в .NET представляет собой набор полей, склеенных друг с другом, и которые для некоторых целей могут рассматриваться как единое целое. Платформа .NET Framework разработана для того, чтобы структуры могли использоваться почти так же, как объекты классов, и в некоторых случаях это может быть удобно.

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

Основные принципы, которые следует учитывать при использовании структур:

  1. Не передавайте и не возвращайте структуры по значению и не заставляйте их копировать без необходимости.

  2. Если придерживаться принципа № 1, большие структуры будут работать так же хорошо, как и маленькие.

Если Alphablobэто структура , содержащая 26 общественных intполей с именем аз, и свойство геттер , abкоторая возвращает сумму своих aи bполями, то данный Alphablob[] arr; List<Alphablob> list;код int foo = arr[0].ab + list[0].ab;нужно будет прочитать поля aи bиз arr[0], но нужно будет прочитать все 26 полей , list[0]даже если это будет игнорировать все, кроме двух из них. Если кто-то хочет иметь общую коллекцию, подобную списку, которая может эффективно работать с подобной структурой alphaBlob, следует заменить индексированный метод получения методом:

delegate ActByRef<T1,T2>(ref T1 p1, ref T2 p2);
actOnItem<TParam>(int index, ActByRef<T, TParam> proc, ref TParam param);

который затем будет вызывать proc(ref backingArray[index], ref param);. Учитывая такую ​​коллекцию, если заменить sum = myCollection[0].ab;на

int result;
myCollection.actOnItem<int>(0, 
  ref (ref alphaBlob item, ref int dest)=>dest = item,
  ref result);

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

К сожалению, .NET Framework не определяет ни один из видов делегатов, необходимых для того, чтобы эта штука работала хорошо, и синтаксис оказывается в некотором беспорядке. С другой стороны, этот подход позволяет избежать ненужного копирования структур, что, в свою очередь, сделает практичным выполнение действий на месте с большими структурами, хранящимися в массивах, что является наиболее эффективным способом доступа к хранилищу.

Supercat
источник
Привет, вы разместили свой ответ на неправильной странице?
Пихо
@pojo: Возможно, мне следовало бы прояснить, что цель ответа - определить ситуацию, в которой использование свойств, а не полей, плохо, и определить, как можно достичь производительности и семантических преимуществ полей, при этом все еще инкапсулируя доступ.
суперкат
@piojo: Мое редактирование проясняет ситуацию?
суперкат
«нужно будет прочитать все 26 полей списка [0], даже если они будут игнорировать все, кроме двух». что? Вы уверены? Вы проверяли MSIL? C # почти всегда передает типы классов по ссылке.
pjc50
@ pjc50: чтение свойства дает результат по значению. Если и индексированный метод получения для listсвойства, и метод получения свойства для abоба встроены, JITter мог бы распознать, что только члены aи bнеобходимы, но я не знаю, какая версия JITter действительно делает такие вещи.
суперкат