Чи рекомендовано 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; }там, де є перевага в показанні наміченого використання.)

Я усвідомлюю, що передчасна оптимізація погана, але, як я вже сказав, у розробці ігор ви не робите щось повільне, коли подібні зусилля можуть зробити це швидким.


34
Завжди проконсультуйтеся з профілером, перш ніж вважати, що щось є "повільним". Мені сказали, що щось було "повільним", і профайлер сказав мені, що треба було виконати 500 000 000 разів, щоб втратити пів секунди. Оскільки він ніколи не виконував би більше декількох сотень разів у кадрі, я вирішив, що це не має значення.
Алмо

5
Я виявив, що трохи абстрагування тут і там допомагає ширше застосовувати оптимізацію низького рівня. Наприклад, я бачив численні проекти простого С, використовуючи різні види пов'язаних списків, які страшенно повільні порівняно з суміжними масивами (наприклад, резервна копія ArrayList). Це пояснюється тим, що C не має дженерики, і цим програмістам C, ймовірно, було легше повторно реалізовувати пов'язані списки, ніж робити те ж саме для ArrayListsалгоритму зміни розміру. Якщо ви придумали хорошу оптимізацію низького рівня, інкапсулюйте її!
hegel5000

3
@Almo Чи застосовуєте ви ту саму логіку до операцій, які відбуваються в 10000 різних місцях? Тому що це доступ для учасників класу. За логікою, ви повинні помножити вартість продуктивності на 10K, перш ніж вирішити, що клас оптимізації не має значення. І 0,5 мс - краще правило, коли продуктивність вашої гри буде зіпсована, а не півсекунди. Ваша думка все ще стоїть, хоча я не думаю, що правильні дії є настільки чіткими, як ви це робите.
piojo

5
У вас 2 коні. Перегони їх.
Щогла

@piojo Я не хочу. Деякі думки повинні завжди вникати в ці речі. У моєму випадку це було циклів у коді інтерфейсу. Один екземпляр інтерфейсу, кілька циклів, кілька ітерацій. Для запису я підтримав ваш коментар. :)
Алмо

Відповіді:


44

Але моє враження полягає в тому, що в розробці ігор, якщо у вас немає причин інакше, все повинно бути якомога швидше.

Не обов'язково. Як і в прикладному програмному забезпеченні, в грі є код, який є критичним для продуктивності, а код - ні.

Якщо код виконується кілька тисяч разів на кадр, то така оптимізація низького рівня може мати сенс. (Хоча ви, можливо, спершу хочете перевірити, чи потрібно насправді викликати так часто. Найшвидший код - це код, який ви не запускаєте)

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

Я читав, що Unity 3D не впевнений у вбудованому доступі через властивості, тому використання властивості призведе до додаткового виклику функції.

Завдяки тривіальним властивостям у int Foo { get; private set; }вашому стилі ви можете бути впевнені, що компілятор оптимізує обгортку властивості. Але:

  • якщо у вас є реалізація, то ви вже не можете бути впевнені в цьому. Чим складніша реалізація, тим менша ймовірність, що компілятор зможе її вбудувати.
  • Властивості можуть приховувати складність. Це меч з двома кінцями. З одного боку, це робить ваш код більш читабельним та змінним. Але з іншого боку, інший розробник, який має доступ до властивості вашого класу, може подумати, що вони просто отримують доступ до однієї змінної і не усвідомлюють, скільки коду ви насправді ховали за цією властивістю. Коли властивість починає обчислюватися дорого, я схильний переробляти її на явний GetFoo()/ SetFoo(value)метод: Я маю на увазі, що за кулісами відбувається набагато більше. (або якщо мені потрібно бути ще впевненішим, що інші отримують підказку: CalculateFoo()/ SwitchFoo(value))

Доступ до поля в загальнодоступному публічному полі типу типу структури є швидким, навіть якщо це поле знаходиться в структурі, яка знаходиться в межах структури тощо, за умови, що об'єкт верхнього рівня або клас, не лише для читання поле або вільно стоячу змінну. Структури можуть бути досить великими без негативного впливу на продуктивність, якщо ці умови застосовуються. Порушення цієї картини призведе до уповільнення, пропорційного розміру структури.
supercat

5
"тоді я схильний перетворювати його на явний метод GetFoo () / SetFoo (value):" - Добре, я схильний переміщувати важкі операції із властивостей у методи.
Марк Роджерс

Чи є спосіб підтвердити, що компілятор Unity 3D робить оптимізацію, яку ви згадуєте (в IL2CPP і Mono будує для Android)?
piojo

9
@piojo Як і у більшості питань щодо продуктивності, найпростіша відповідь - "просто роби це". У вас готовий код - протестуйте різницю між наявністю поля та властивістю. Деталі реалізації варто лише вивчити, коли ви зможете фактично виміряти важливу різницю між двома підходами. На мій досвід, якщо ви пишете, щоб все було якомога швидше, ви обмежуєте доступні оптимізації високого рівня та просто намагаєтеся перейти на низькі рівні ("не можна побачити ліс для дерев"). Наприклад, знайти способи Updateповністю пропустити, ви заощадите більше часу, ніж Updateшвидкий.
Луань

9

Коли ви сумніваєтесь, використовуйте кращі практики.

Ви сумніваєтесь.


Це була проста відповідь. Реальність, звичайно, складніша.

По-перше, існує міф про те, що ігрове програмування - це надвисокопродуктивне програмування, і все повинно бути якомога швидше. Я класифікую ігрове програмування як програмування з урахуванням продуктивності . Розробник повинен знати про обмеження продуктивності та працювати в них.

По-друге, є міф, що зробити кожну річ якомога швидшою - це те, що робить програму швидкою. Насправді виявлення та оптимізація вузьких місць - це те, що робить програму швидкою, а це набагато простіше / дешевше / швидше зробити, якщо код легко підтримувати та змінювати; надзвичайно оптимізований код важко підтримувати та змінювати. Звідси випливає, що для того, щоб писати надзвичайно оптимізовані програми, ви повинні використовувати екстремальну оптимізацію коду якомога частіше та якомога рідше.

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

І по-четверте, особисто я не дотримуюся найкращої практики Microsoft у цьому випадку. Якщо мені потрібна власність із тривіальними та загальнодоступними дітьми та сетерами, я просто використовую поле та просто змінюю його на властивість, коли мені потрібно. Однак мені дуже рідко потрібна така тривіальна властивість - набагато частіше, що я хочу перевірити граничні умови або хочу зробити сеттер приватним.


2

Інтернету для виконання .net дуже просто вбудувати прості властивості, і часто це відбувається. Але це вкладення зазвичай відключається профілями, отже, їх легко ввести в оману і подумати, що зміна публічної власності на публічну сферу пришвидшить програмне забезпечення.

У коді, який викликається іншим кодом, який не буде перекомпільований при перекомпіляції коду, є хороший випадок використання властивостей, оскільки перехід від поля до властивості - це переломна зміна. Але для більшості кодів, окрім можливості встановити точку перерви на get / set, я ніколи не бачив переваг у використанні простої властивості порівняно з публічним полем.

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


2

Структури та відкриті поля полегшать сумісність з деякими не керованими API. Часто ви виявите, що API низького рівня хоче отримати доступ до значень за посиланням, що добре для продуктивності (оскільки ми уникаємо зайвої копії). Використання властивостей є перешкодою до цього, і часто бібліотеки обгортки роблять копії для зручності використання, а іноді і для безпеки.

Через це ви часто будете отримувати кращі показники, маючи векторні та матричні типи, які не мають властивостей, а голі поля.


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

У цьому випадку у нас є пара:

  • Властивість дозволяє змінювати реалізацію без зміни коду клієнта (на бінарному рівні можна змінити поле на властивість без зміни коду клієнта на вихідному рівні, проте після зміни він буде компілюватися на щось інше) . Це означає, що, використовуючи властивість з самого початку, код, на який посилається ваш, не доведеться перекомпілювати лише для того, щоб змінити те, що робить властивість внутрішньо.

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

Я казав код клієнта. Це означає код, який викликає ваш. Якщо ви не створюєте бібліотеку (або навіть створюєте бібліотеку, але використовуєте внутрішню, а не загальнодоступну), зазвичай ви можете піти з нею і з хорошою дисципліною. У цій ситуації найкраща практика використання властивостей є для запобігання стрілянині в ногу. Крім того, міркувати про код набагато простіше, якщо ви бачите всі місця, де поле може змінюватись в одному файлі, замість того, щоб турбуватися про те, що воно чи не змінюється десь в іншому місці. Насправді властивості також добре ставити точки прориву, коли ви з'ясовуєте, що пішло не так.


Так, є цінність бачити те, що робиться в галузі. Однак у вас є мотивація йти проти кращих практик? чи ти просто противодишся найкращим практикам - ускладнюючи розгляд коду - просто тому, що це робив хтось інший? Ах, до речі, "інші роблять це" - це те, як ви починаєте вантажний культ.


Отже ... Ваша гра працює повільно? Вам краще приділити час, щоб з'ясувати вузьке місце і виправити це, а не міркувати, що це може бути. Ви можете бути впевнені, що компілятор зробить багато оптимізацій, через це, швидше за все, ви переглядаєте неправильну проблему.

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


Нарешті, чи заробляєте ви щось, ідучи проти кращих практик?

Для деталей вашого випадку (Unity та Mono для Android), чи Unity приймає значення за посиланням? Якщо це не так, він все одно скопіює значення, жодного посилення продуктивності там немає.

Якщо це так, якщо ви передаєте ці дані в API, який приймає посилання. Чи має сенс оприлюднювати поле, або ви можете зробити тип, здатний викликати API безпосередньо?

Так, звичайно, можуть бути оптимізації, які можна зробити, використовуючи структури з оголеними полями. Наприклад, ви отримуєте доступ до них за допомогою покажчиків Span<T> чи подібних. Вони також компактні в пам'яті, що дозволяє їх легко серіалізувати для надсилання по мережі або для постійного зберігання (і так, це копії).

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


1

... моє враження, що в розробці ігор, якщо у вас немає причин інакше, все повинно бути якомога швидше.

:

Я усвідомлюю, що передчасна оптимізація погана, але, як я вже сказав, у розробці ігор ви не робите щось повільне, коли подібні зусилля можуть зробити це швидким.

Ви маєте рацію усвідомлювати, що передчасна оптимізація погана, але це лише половина історії (див. Повну цитату нижче). Навіть у розробці ігор не все повинно бути максимально швидким.

Спочатку змусьте це працювати. Тільки тоді оптимізуйте біти, які їй потрібні. Це не означає, що перший проект може бути безладним або непотрібним, але оптимізація не є пріоритетним на цьому етапі.

" Справжня проблема полягає в тому, що програмісти витратили занадто багато часу, переймаючись ефективністю в неправильних місцях і в неправильний час ; передчасна оптимізація є коренем усього зла (або, принаймні, більшості) в програмуванні". Дональд Кнут, 1974 рік.


1

Для таких базових речей оптимізатор C # все одно зрозуміє. Якщо ви переживаєте з цього приводу (і якщо ви переживаєте за нього більше 1% свого коду, ви робите це неправильно ), для вас було створено атрибут. Атрибут [MethodImpl(MethodImplOptions.AggressiveInlining)]значно збільшує ймовірність того, що метод буде вбудований (властивості і налаштування властивостей - це методи).


0

Виставлення членів типу структури як властивостей, а не полів часто дасть низьку продуктивність та семантику, особливо у випадках, коли структури насправді трактуються як структури, а не як "химерні об'єкти".

Структура в .NET - це сукупність полів, скріплених каналами разом, і які для певних цілей можна розглядати як одиницю. .NET Framework розроблена так, що дозволяє використовувати структури так само, як і об'єкти класу, і буває, що це може бути зручно.

Значна частина порад щодо оточуючих структур базується на уявленні, що програмісти будуть бажати використовувати структури як об'єкти, а не як колекції полів, пов'язаних разом. З іншого боку, є багато випадків, коли може бути корисно мати щось, що поводиться як з’єднана купою полів. Якщо цього потрібно, порада .NET додасть не потрібну роботу програмістам і компіляторам, приносячи гірші показники продуктивності та семантики, ніж просто використання структур безпосередньо.

Найбільші принципи, які слід пам’ятати при використанні структур:

  1. Не передайте і не повертайте структури за значенням або іншим чином не спричиняйте їх копіювання без потреби.

  2. Якщо дотримуватися принципу №1, великі структури будуть працювати так само добре, як і маленькі.

Якщо Alphablobце структура, що містить 26 публічних intполів з назвою az, та отримувач властивостей, abякий повертає суму її aта bполів, то дається Alphablob[] arr; List<Alphablob> list;, код int foo = arr[0].ab + list[0].ab;повинен буде читати поля aта bof 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параметрів, компілятор може передавати статичний делегат.

На жаль. З іншого боку, такий підхід дає можливість уникнути копіювання структур без необхідності, що, в свою чергу, зробить практичним виконання на місці дій на великих структурах, що зберігаються в масивах, що є найбільш ефективним способом доступу до сховища.


Привіт, ти написав свою відповідь на неправильній сторінці?
piojo

@pojo: Можливо, я повинен був би зрозуміти, що мета відповіді - визначити ситуацію, коли використання властивостей, а не полів є поганим, та визначити, як можна досягти продуктивності та семантичних переваг полів, при цьому все-таки інкапсулюючи доступ.
supercat

@piojo: Чи редагування робить ясніше речі?
supercat

"потрібно буде прочитати всі 26 полів списку [0], хоча він буде ігнорувати всі, крім двох." що? Ти впевнений? Ви перевірили MSIL? C # майже завжди передає типи класів за посиланням.
pjc50

@ pjc50: Читання властивості дає результат за значенням. Якщо і індексований геттер для, listі власник власності для abобох є вкладеними, можливо, JITter може визнати, що потрібні лише члени aта bпотрібні, але я не знаю, що будь-яка версія JITter насправді робить такі речі.
supercat
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.