Як я повинен обліковувати GC під час створення ігор з Unity?


15

* Наскільки я знаю, Unity3D для iOS базується на режимі Mono, а Mono має лише покоління та рейтинг GC.

Ця система GC не може уникнути часу GC, який зупиняє ігрову систему. Об'єднання об'єднань може зменшити це, але не повністю, оскільки ми не можемо контролювати, коли екземпляри трапляються в бібліотеці базового класу CLR. Ці приховані маленькі та часті випадки в кінцевому підсумку піднімуть недетермінований час GC. Періодично примушуючи повний GC значно погіршить продуктивність (чи може Mono змусити завершити GC, власне?)

Отже, як я можу уникнути цього часу GC під час використання Unity3D без величезних погіршень продуктивності?


3
Останній Unity Pro Profiler включає скільки сміття утворюється в певних функціональних дзвінках. Ви можете скористатися цим інструментом, щоб допомогти побачити інші місця, які можуть створити сміття, інакше може бути не очевидно відразу.
Тетрад

Відповіді:


18

На щастя, як ви вказали, складений COMPACT Mono використовує генераційний GC (на відміну від таких, як Microsoft, як WinMo / WinPhone / XBox, які просто підтримують плоский список).

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

Передчасна оптимізація

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

Об'єднання об'єднань Дорогі типи посилань

Вам слід об'єднати довідкові типи, які ви часто створюєте, або які мають глибокі структури. Прикладом кожного може бути:

  • Створюється часто: BulletОб'єкт у грі з кулею .
  • Глибока структура: Дерево рішень для реалізації ШІ.

Ви повинні використовувати a Stackяк свій пул (на відміну від більшості реалізацій, які використовують a Queue). Причина цього полягає в тому, що з, Stackякщо ви повертаєте об'єкт у басейн, а щось інше негайно хапає його; у нього буде набагато більший шанс опинитися на активній сторінці - або навіть у кеші CPU, якщо вам пощастить. Це просто крихітний трішки швидше. Крім того, завжди обмежуйте розмір своїх басейнів (просто ігноруйте "чеки", якщо ваш ліміт перевищено).

Уникайте створення нових списків, щоб очистити їх

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

Використовуйте масиви структури (або списки), де це можливо

Ви отримуєте невелику вигоду від структур використання (або типів цінності взагалі), якщо передаєте їх між об'єктами. Наприклад, у більшості «хороших» частинок системи окремі частинки зберігаються у масивному масиві: масив та індекс передаються навколо замість самої частинки. Причина, яка працює так добре, полягає в тому, що коли GC потрібно зібрати масив, він може повністю пропустити вміст (це примітивний масив - тут нічого робити). Тож замість того, щоб дивитись на 10 000 об’єктів, GC просто повинен подивитися на 1 масив: величезний виграш! Знову ж таки, це буде працювати лише з типами значень .

Після RoyT. за умови життєздатних та конструктивних відгуків, я вважаю, що мені потрібно розширити цю проблему. Ви повинні використовувати цю техніку лише тоді, коли маєте справу з величезною кількістю сутностей (від тисяч до десятків тисяч). Крім того, вона повинна бути структурою, яка не повинна мати жодних посилальних типів полів і повинна містити явно набраний масив. На відміну від його відгуків, ми розміщуємо його в масиві, який, швидше за все, є полем у класі - це означає, що він збирається висадити купу (ми не намагаємося уникати розподілу купи - просто уникаємо роботи GC). Нас дійсно хвилює той факт, що це суцільний шматок пам’яті з великою кількістю значень, на який GC може просто дивитися в O(1)операції замість O(n)операції.

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

GC.Collect ()

Це, безумовно, НАЙКРАЩИЙ спосіб застрелити себе в ногу (див. "Розгляд продуктивності") з поколінням GC. Ви повинні викликати це лише тоді, коли ви створили ЕКСТРЕМУ кількість сміття - і один екземпляр, де це могло бути проблемою, - це лише після того, як ви завантажили вміст на рівні - і навіть тоді ви, мабуть, повинні збирати лише перше покоління ( GC.Collect(0);) сподіваємось запобігти просуванню об’єктів третьому поколінню.

Ідентифікаційний і полевий обнулення

Варто обнулити поля, коли вам більше не потрібен об'єкт (тим більше на обмежених об'єктах). Причина полягає в подробицях того, як працює GC: він видаляє лише об'єкти, які не вкорінюються (тобто посилаються), навіть якщо цей об’єкт був би вкорінений через інші об’єкти, видалені в поточній колекції ( примітка: це залежить від GC аромат у використанні - деякі фактично чистять ланцюги). Крім того, якщо об’єкт вижив у колекції, його негайно просувають до наступного покоління - це означає, що будь-які об’єкти, що залишаються навколо полів, будуть рекламовані під час колекції. Кожне наступне покоління експоненціально дорожче збирати (і трапляється так само рідко).

Візьмемо такий приклад:

MyObject (G1) -> MyNestedObject (G1) -> MyFurtherNestedObject (G1)
// G1 Collection
MyNestObject (G2) -> MyFurtherNestedObject (G2)
// G2 Collection
MyFurtherNestedObject (G3)

Якщо MyFurtherNestedObjectміститься багатомігабайтний об'єкт, вам можна гарантувати, що GC не буде дивитися на нього досить довго - тому що ви ненароком пересунули його на G3. Порівняйте це з цим прикладом:

MyObject (G1) -> MyNestedObject (G1) -> MyFurtherNestedObject (G1)
// Dispose
MyObject (G1)
MyNestedObject (G1)
MyFurtherNestedObject (G1)
// G1 Collection

Шаблон Disposer допомагає вам встановити передбачуваний спосіб запитувати об'єкти, щоб очистити їхні приватні поля. Наприклад:

public class MyClass : IDisposable
{
    private MyNestedType _nested;

    // A finalizer is only needed IF YOU CONTROL UNMANAGED RESOURCES
    // ~MyClass() { }

    public void Dispose()
    {
       _nested = null;
    }
}

1
WTF ви про це? .NET Framework для Windows абсолютно покоління. У їхньому класі навіть є методи GetGeneration .
DeadMG

2
.NET в Windows використовує генераторний смітник, проте .NET Compact Framework, який використовується на WP7 та Xbox360, має більш обмежений GC, хоча це, мабуть, більше, ніж плоский список, але не повний покоління.
Рой Т.

Джонатан Дікінсон: Я хотів би додати, що структури - це не завжди відповідь. У .Net і так, ймовірно, також Mono структура не обов'язково живе на стеку (що добре), але також може жити на купі (саме там вам потрібен смітник). Правила для цього досить складні, але в цілому це трапляється, коли структура містить нецінні типи.
Рой Т.

1
@RoyT. дивись вище коментар. Я зазначив, що структури корисні для великої кількості вкладишів у масиві - як система частинок. Якщо ви переживаєте за структури GC в масиві - це шлях; для таких даних, як частинки.
Джонатан Дікінсон

10
@DeadMG також WTF дуже не вимагає - вважаючи, що ви насправді неправильно не прочитали відповідь. Будь ласка, заспокойтесь і перечитайте відповідь, перш ніж писати ненависні коментарі в загальноприйнятій громаді, як це.
Джонатан Дікінсон

7

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

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

Ви також можете примусити збирання сміття в певний час, що може допомогти чи не допомогти: дивіться деякі пропозиції тут: http://unity3d.com/support/documentation/Manual/iphone-Optimizing-Scripts.html


2
Вам слід дійсно пояснити наслідки примусового використання ГК. Це грає в хаос із евристикою та компонуванням GC та може призвести до більших проблем із пам'яттю при неправильному використанні: спільнота XNA / MVP / персонал зазвичай вважає вміст після завантаження єдиним розумним часом для примусового збору. Вам слід уникати згадки про це повністю, якщо ви присвячуєте лише одне речення. GC.Collect()= вільна пам'ять зараз, але, швидше за все, проблеми пізніше.
Джонатан Дікінсон

Ні, ви повинні пояснити наслідки примушування GC, оскільки ви знаєте про це більше, ніж я. :) Однак майте на увазі, що Mono GC не обов'язково є таким же, як Microsoft GC, і ризики можуть бути не однаковими.
Кілотан

Все-таки +1. Я продовжував і детально зупинився на особистому досвіді роботи з GC - який вам може бути цікавим.
Джонатан Дікінсон
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.