Как я должен учитывать GC при создании игр с Unity?

15

* Насколько я знаю, Unity3D для iOS основана на среде исполнения Mono, а в Mono есть только GC Mark Mark and Sweep GC.

Эта система GC не может избежать времени GC, которое останавливает игровую систему. Пул экземпляров может уменьшить это, но не полностью, потому что мы не можем контролировать создание экземпляров в библиотеке базовых классов CLR. Эти скрытые маленькие и частые случаи в конечном итоге приведут к недетерминированному времени GC. Периодическое принудительное завершение GC значительно ухудшит производительность (может ли Mono принудительно завершить GC?)

Итак, как я могу избежать этого времени GC при использовании Unity3D без значительного снижения производительности?

Eonil
источник
3
В последней версии Unity Pro Profiler указано, сколько мусора генерируется при определенных вызовах функций. Вы можете использовать этот инструмент, чтобы увидеть другие места, которые могут генерировать мусор, который в противном случае может быть неочевидным.
Тетрад

Ответы:

18

К счастью, как вы указали, сборки COMPACT Mono используют GC поколения (в отличие от Microsoft, например, WinMo / WinPhone / XBox, которые просто поддерживают плоский список).

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

Преждевременная оптимизация

Сначала убедитесь, что это действительно проблема для вас, прежде чем пытаться ее исправить.

Объединение дорогих справочных типов

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

  • Создано часто: Bulletобъект в игре пуля-ада .
  • Глубокая структура: дерево решений для реализации ИИ.

Вы должны использовать в Stackкачестве пула (в отличие от большинства реализаций, которые используют Queue). Причина этого в том, что Stackесли вы возвращаете объект в пул, и что-то еще немедленно его захватывает; у него будет гораздо больше шансов оказаться на активной странице - или даже в кэше процессора, если вам повезет. Это немного быстрее. Кроме того, всегда ограничивайте размер ваших пулов (просто игнорируйте 'checkins', если ваш лимит был превышен).

Избегайте создания новых списков, чтобы очистить их

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

Используйте массивы структур (или списки), где это возможно

Вы получаете небольшую выгоду от использования структур (или типов значений в целом), если вы передаете их между объектами. Например, в большинстве «хороших» систем частиц отдельные частицы хранятся в массиве: массив и индекс передаются вместо самой частицы. Причина, по которой это работает так хорошо, заключается в том, что когда GC нужно собрать массив, он может полностью пропустить содержимое (это примитивный массив - здесь ничего не нужно делать). Поэтому вместо 10 000 объектов GC просто нужно посмотреть на 1 массив: огромный выигрыш! Опять же, это будет работать только с типами значений .

После РойТ. предоставив некоторую жизнеспособную и конструктивную обратную связь, я чувствую, что мне нужно подробнее остановиться на этом. Вы должны использовать эту технику только тогда, когда имеете дело с огромным количеством сущностей (от тысяч до десятков тысяч). Кроме того, это должна быть структура, которая не должна иметь никаких полей ссылочного типа и должна находиться в массиве с явным типом. Вопреки его отзывам, мы помещаем его в массив, который, скорее всего, является полем в классе - это означает, что он собирается занять кучу (мы не пытаемся избежать выделения кучи - просто избегаем работы GC). Мы действительно заботимся о том, что это непрерывный кусок памяти с большим количеством значений, которые GC может просто посмотреть в O(1)операции вместо O(n)операции.

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

GC.Collect ()

Это, безусловно, ЛУЧШИЙ способ выстрелить себе в ногу (см. «Вопросы производительности») с помощью GC поколения. Вы должны вызывать его только тогда, когда вы создали ОЧЕНЬ большое количество мусора - и единственный случай, когда это может быть проблемой, - это сразу после загрузки контента для уровня - и даже тогда вам, вероятно, следует собирать только первое поколение ( GC.Collect(0);) Надеемся, что предотвратить продвижение объектов в третьем поколении.

IDisposable и обнуление поля

Стоит обнулять поля, когда вам больше не нужен объект (особенно на ограниченных объектах). Причина в деталях того, как работает GC: он удаляет только те объекты, которые не являются корневыми (то есть ссылаются), даже если этот объект был бы удален из-за удаления других объектов в текущей коллекции ( примечание: это зависит от GC аромат в использовании - некоторые действительно убирают цепи). Кроме того, если объект переживает коллекцию, он немедленно продвигается до следующего поколения - это означает, что любые объекты, оставленные в полях, будут продвигаться во время коллекции. Каждое последующее поколение экспоненциально дороже собирать (и это происходит нечасто).

Возьмите следующий пример:

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

Если вы MyFurtherNestedObjectсодержали объект размером в несколько мегабайт, вы можете быть уверены, что ГХ не будет смотреть на него в течение достаточно долгого времени - потому что вы непреднамеренно повысили его до 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 использует Generative Garbage Collector, однако .NET Compact Framework, используемый в WP7 и Xbox360, имеет более ограниченный сборщик мусора, хотя, вероятно, это не просто плоский список, а не полный.
Рой Т.
Джонатан Дикинсон: Я хотел бы добавить, что структуры не всегда являются ответом. В .Net и так, вероятно, также Mono структура не обязательно живет в стеке (что хорошо), но может также жить в куче (где вам нужен сборщик мусора). Правила для этого достаточно сложны, но в целом это происходит, когда структура содержит не значащие типы.
Рой Т.
1
@RoyT. см. выше комментарий. Я указал, что структуры хороши для большого количества линейных данных в массиве - как система частиц. Если вы беспокоитесь о структурах GC в массиве, это путь; для данных, как частицы.
Джонатан Дикинсон
10
@DeadMG также WTF крайне не рекомендуется - учитывая, что вы на самом деле не прочитали ответ правильно. Пожалуйста, успокойтесь и перечитайте ответ, прежде чем писать ненавистные комментарии в общепринятом сообществе, подобном этому.
Джонатан Дикинсон
7

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

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

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

Kylotan
источник
2
Вы должны действительно объяснить последствия принуждения GC. Он разрушает эвристику и компоновку GC и может привести к большим проблемам с памятью при неправильном использовании: сообщество XNA / MVP / персонала обычно считает пост-загрузку контента единственным разумным временем для принудительного сбора данных. Вы действительно должны избегать упоминания этого полностью, если вы посвятите только одно предложение этому вопросу. GC.Collect()= свободная память сейчас, но скорее всего проблемы позже.
Джонатан Дикинсон
Нет, вы должны объяснить последствия принуждения GC, потому что вы знаете об этом больше, чем я. :) Однако имейте в виду, что Mono GC не обязательно совпадает с Microsoft GC, и риски могут не совпадать.
Kylotan
Тем не менее +1. Я пошел дальше и остановился на некотором личном опыте с GC, который может показаться вам интересным.
Джонатан Дикинсон