Wie sollte ich den GC berücksichtigen, wenn ich Spiele mit Unity baue?

15

* Soweit ich weiß, basiert Unity3D für iOS auf der Mono-Laufzeit und Mono verfügt nur über eine generationsübergreifende Mark & ​​Sweep-GC.

Dieses GC-System kann die GC-Zeit nicht umgehen, die das Spielsystem stoppt. Instanz-Pooling kann dies jedoch nicht vollständig reduzieren, da die Instanziierung in der Basisklassenbibliothek der CLR nicht gesteuert werden kann. Diese versteckten kleinen und häufigen Instanzen erhöhen schließlich die unbestimmte GC-Zeit. Wenn Sie die vollständige GC regelmäßig erzwingen, wird die Leistung erheblich beeinträchtigt (kann Mono die vollständige GC tatsächlich erzwingen?)

Wie kann ich diese GC-Zeit vermeiden, wenn ich Unity3D verwende, ohne die Leistung zu beeinträchtigen?

Eonil
quelle
3
Der neueste Unity Pro Profiler enthält, wie viel Müll bei bestimmten Funktionsaufrufen erzeugt wird. Sie können dieses Tool verwenden, um andere Stellen zu erkennen, an denen möglicherweise Müll entsteht, der ansonsten möglicherweise nicht sofort erkennbar ist.
Tetrad

Antworten:

18

Wie Sie bereits betont haben, verwenden die COMPACT Mono-Builds zum Glück einen Generations-GC (im Gegensatz zu den Microsoft-Builds wie WinMo / WinPhone / XBox, die nur eine flache Liste führen).

Wenn Ihr Spiel einfach ist, sollte der GC es in Ordnung bringen, aber hier sind einige Hinweise, die Sie sich ansehen sollten.

Vorzeitige Optimierung

Stellen Sie zunächst sicher, dass dies tatsächlich ein Problem für Sie ist, bevor Sie versuchen, es zu beheben.

Pooling teurer Referenztypen

Sie sollten Referenztypen bündeln, die Sie entweder häufig erstellen oder die über tiefe Strukturen verfügen. Ein Beispiel von jedem wäre:

  • Oft erschaffen: Ein BulletObjekt in einem Bullet-Hell-Spiel .
  • Tiefe Struktur: Entscheidungsbaum für eine AI-Implementierung.

Sie sollten a Stackals Ihren Pool verwenden (im Gegensatz zu den meisten Implementierungen, die a verwenden Queue). Der Grund dafür ist, dass Sie mit a Stackein Objekt in den Pool zurückgeben und etwas anderes es sofort ergreift. Es hat eine viel höhere Chance, auf einer aktiven Seite zu sein - oder sogar im CPU-Cache, wenn Sie Glück haben. Es ist nur ein bisschen schneller. Begrenzen Sie außerdem immer die Größe Ihrer Pools (ignorieren Sie einfach das Einchecken, wenn Ihr Limit überschritten wurde).

Vermeiden Sie das Erstellen neuer Listen, um diese zu löschen

Erstellen Sie keine neuen, Listwenn Sie es tatsächlich wollten Clear(). Sie können das Back-End-Array wiederverwenden und eine Menge Array-Zuordnungen und Kopien speichern. Ähnlich wie bei diesem Versuch, Listen mit einer sinnvollen Anfangskapazität zu erstellen (denken Sie daran, dies ist kein Limit - nur eine Anfangskapazität) - es muss nicht genau sein, nur eine Schätzung. Dies sollte grundsätzlich für jede Sammlungsart gelten - mit Ausnahme von a LinkedList.

Verwenden Sie nach Möglichkeit Struct Arrays (oder Listen)

Sie profitieren nur wenig von der Verwendung von Strukturen (oder Werttypen im Allgemeinen), wenn Sie diese zwischen Objekten austauschen. Beispielsweise werden in den meisten "guten" Partikelsystemen die einzelnen Partikel in einem massiven Array gespeichert: Das Array und der Index werden anstelle des Partikels selbst herumgereicht. Der Grund, warum dies so gut funktioniert, ist, dass der GC, wenn er das Array sammeln muss, den Inhalt vollständig überspringen kann (es ist ein primitives Array - hier nichts zu tun). Anstatt also 10 000 Objekte zu betrachten, muss der GC nur 1 Array betrachten: enormer Gewinn! Auch dies funktioniert nur mit Werttypen .

Nach RoyT. Ich habe einige brauchbare und konstruktive Rückmeldungen gegeben, von denen ich glaube, dass ich sie weiter ausbauen muss. Sie sollten diese Technik nur anwenden, wenn Sie mit einer großen Anzahl von Entitäten zu tun haben (Tausende bis Zehntausende). Darüber hinaus es muss eine Struktur sein , die nicht müssen keine Referenztyp Felder haben und in einem explizit typisierte Array leben müssen. Im Gegensatz zu seinem Feedback platzieren wir es in einem Array, bei dem es sich sehr wahrscheinlich um ein Feld in einer Klasse handelt - was bedeutet, dass es auf dem Heap landet (wir versuchen nicht, eine Heap-Zuweisung zu vermeiden - lediglich GC-Arbeit zu vermeiden). Uns ist besonders wichtig, dass es sich um einen zusammenhängenden Speicherblock mit vielen Werten handelt, den der GC einfach in einer O(1)Operation anstelle einer O(n)Operation betrachten kann.

Sie sollten auch diese Arrays so nah an Ihren Start der Anwendung wie möglich verteilen die Chancen zu mildern Fragmentierung auftritt, oder übermäßige Arbeit als die GC diese Stücke zu bewegen versucht , um (und betrachten mit einer Hybrid - Linked - List anstelle der eingebauten in ListArt ).

GC.Collect ()

Dies ist definitiv DIE BESTE Möglichkeit, sich mit einem Generations-GC in den Fuß zu schießen (siehe: "Leistungsaspekte"). Sie sollten es nur aufrufen, wenn Sie eine EXTREME Menge an Müll erstellt haben - und die einzige Instanz, bei der dies ein Problem sein könnte, ist, nachdem Sie den Inhalt für eine Ebene geladen haben - und selbst dann sollten Sie wahrscheinlich nur die erste Generation sammeln ( GC.Collect(0);) hoffentlich verhindern, dass Objekte in die dritte Generation befördert werden.

IDisposable und Field Nulling

Es lohnt sich, Felder auf Null zu setzen, wenn Sie kein Objekt mehr benötigen (mehr bei eingeschränkten Objekten). Der Grund liegt in den Details der Funktionsweise des GC: Es werden nur Objekte entfernt, die nicht verwurzelt sind (dh auf die verwiesen wird), auch wenn dieses Objekt nicht verwurzelt gewesen wäre, weil andere Objekte in der aktuellen Sammlung entfernt wurden ( Hinweis: Dies hängt vom GC ab Aroma im Gebrauch - einige reinigen tatsächlich Ketten). Wenn ein Objekt eine Sammlung überlebt, wird es sofort zur nächsten Generation befördert. Dies bedeutet, dass alle Objekte, die in Feldern herumliegen, während einer Sammlung befördert werden. Jede nachfolgende Generation ist in der Sammlung exponentiell teurer (und tritt so selten auf).

Nehmen Sie das folgende Beispiel:

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

Wenn Sie MyFurtherNestedObjectein Multi-Megabyte-Objekt enthalten, können Sie sicher sein, dass der GC es nicht lange ansieht, da Sie es versehentlich zu G3 hochgestuft haben. Vergleichen Sie das mit diesem Beispiel:

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

Mit dem Disposer-Muster können Sie vorhersehbare Methoden einrichten, mit denen Objekte aufgefordert werden, ihre privaten Felder zu löschen. Beispielsweise:

public class MyClass : IDisposable
{
    private MyNestedType _nested;

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

    public void Dispose()
    {
       _nested = null;
    }
}
Jonathan Dickinson
quelle
1
WTF bist du etwa? Das .NET Framework unter Windows ist absolut generationsübergreifend. Ihre Klasse verfügt sogar über GetGeneration- Methoden.
DeadMG
2
.NET unter Windows verwendet einen Garbage Collector für Generationen. Das .NET Compact Framework für WP7 und die Xbox360 verfügt jedoch über einen eingeschränkteren GC, obwohl es wahrscheinlich mehr als eine flache Liste ist, ist es keine vollständige Garbage Collector für Generationen.
Roy T.
Jonathan Dickinson: Ich möchte hinzufügen, dass Strukturen nicht immer die Antwort sind. In .Net und damit wahrscheinlich auch in Mono lebt eine Struktur nicht unbedingt auf dem Stack (was gut ist), sondern kann auch auf dem Heap (wo man einen Garbage Collector braucht) leben. Die Regeln dafür sind ziemlich kompliziert, aber im Allgemeinen passiert es, wenn eine Struktur Nicht-Wert-Typen enthält.
Roy T.
1
@RoyT. siehe obigen Kommentar. Ich habe darauf hingewiesen, dass Strukturen für eine große Anzahl von Daten in einem Array geeignet sind - wie ein Partikelsystem. Wenn Sie sich Sorgen um die GC-Strukturen in einem Array machen, sind Sie hier richtig. für Daten wie Partikel.
Jonathan Dickinson
10
@DeadMG auch WTF ist sehr unangebracht - wenn man bedenkt, dass man die Antwort nicht richtig gelesen hat. Bitte beruhigen Sie Ihr Temperament und lesen Sie die Antwort erneut, bevor Sie in einer im Allgemeinen gut erzogenen Community wie dieser hasserfüllte Kommentare verfassen.
Jonathan Dickinson
7

In der Basisbibliothek findet nur eine sehr geringe dynamische Instanziierung statt, es sei denn, Sie rufen etwas auf, das dies erfordert. Die überwiegende Mehrheit der Speicherzuweisung und Freigabe erfolgt über Ihren eigenen Code, den Sie steuern können.

Soweit ich weiß, können Sie dies nicht gänzlich vermeiden. Sie können lediglich sicherstellen, dass Sie Objekte recyceln und bündeln, Strukturen anstelle von Klassen verwenden, das UnityGUI-System vermeiden und generell vermeiden, dass Sie während eines bestimmten Zeitraums neue Objekte im dynamischen Speicher erstellen wenn es auf leistung ankommt.

Sie können die Garbage Collection auch zu bestimmten Zeiten erzwingen, was möglicherweise nicht hilft. Einige Vorschläge finden Sie hier: http://unity3d.com/support/documentation/Manual/iphone-Optimizing-Scripts.html

Kylotan
quelle
2
Sie sollten die Auswirkungen des Erzwingens von GC wirklich erklären. Es wirkt sich negativ auf die GC-Heuristik und das Layout aus und kann bei falscher Verwendung zu größeren Speicherproblemen führen: Die XNA / MVPs / Mitarbeiter-Community betrachtet das Nachladen von Inhalten im Allgemeinen als die einzig sinnvolle Zeit, um eine Erfassung zu erzwingen. Sie sollten es wirklich vermeiden, es ganz zu erwähnen, wenn Sie der Sache nur einen Satz widmen. GC.Collect()= freier Speicher jetzt, aber sehr wahrscheinlich später Probleme.
Jonathan Dickinson
Nein, Sie sollten die Auswirkungen des Erzwingens von GC erklären, da Sie mehr darüber wissen als ich. :) Beachten Sie jedoch, dass der Mono-GC nicht unbedingt mit dem Microsoft-GC identisch ist und die Risiken möglicherweise nicht dieselben sind.
Kylotan,
Immer noch +1. Ich ging voran und ging auf einige persönliche Erfahrungen mit dem GC ein, die Sie vielleicht interessant finden.
Jonathan Dickinson