Jak powinienem uwzględnić GC podczas tworzenia gier w Unity?

15

* O ile mi wiadomo, Unity3D na iOS opiera się na środowisku wykonawczym Mono, a Mono ma tylko generacyjną markę i Sweep GC.

Ten system GC nie może uniknąć czasu GC, który zatrzymuje system gry. Pula wystąpień może to zmniejszyć, ale nie całkowicie, ponieważ nie możemy kontrolować tworzenia wystąpień w bibliotece klas podstawowych CLR. Te ukryte małe i częste instancje ostatecznie spowodują niedeterministyczny czas GC. Wymuszanie pełnego GC okresowo znacznie pogorszy wydajność (czy Mono może wymusić pełne GC?)

Jak więc uniknąć tego czasu GC, gdy korzystam z Unity3D bez znacznego obniżenia wydajności?

Eonil
źródło
3
Najnowszy profil Unity Pro Profiler zawiera ilość śmieci generowanych w niektórych wywołaniach funkcji. Możesz użyć tego narzędzia, aby zobaczyć inne miejsca, które generują śmieci, które w przeciwnym razie nie byłyby oczywiste.
Tetrad

Odpowiedzi:

18

Na szczęście, jak zauważyłeś, kompilacje COMPACT Mono używają generacyjnej GC (w przeciwieństwie do tych Microsoft, takich jak WinMo / WinPhone / XBox, które tylko utrzymują płaską listę).

Jeśli twoja gra jest prosta, GC powinien sobie z tym poradzić, ale oto kilka wskazówek, które warto rozważyć.

Przedwczesna optymalizacja

Najpierw upewnij się, że jest to problem, zanim spróbujesz go naprawić.

Łączenie drogich typów referencyjnych

Powinieneś połączyć typy referencji, które często tworzysz lub które mają głębokie struktury. Przykładem każdego z nich byłoby:

  • Często tworzone: BulletObiekt w grze typu bullet-hell .
  • Głęboka struktura: drzewo decyzyjne dla implementacji AI.

Powinieneś użyć a Stackjako swojej puli (w przeciwieństwie do większości implementacji, które używają a Queue). Powodem tego jest to, że Stackjeśli zwrócisz obiekt do puli i coś innego natychmiast go złapie; będzie miał znacznie większą szansę na bycie na aktywnej stronie - a nawet w pamięci podręcznej procesora, jeśli masz szczęście. Jest tylko trochę szybszy. Ponadto zawsze ograniczaj wielkość swoich pul (zignoruj ​​„meldowanie”, jeśli limit został przekroczony).

Unikaj tworzenia nowych list, aby je wyczyścić

Nie twórz nowego, Listkiedy tak naprawdę chciałeś Clear(). Możesz ponownie użyć tablicy zaplecza i zaoszczędzić mnóstwo przydziałów tablicy i kopii. Podobnie jak w przypadku tej próby, twórz listy o znacznej pojemności początkowej (pamiętaj, że to nie jest limit - tylko pojemność początkowa) - nie musi być dokładna, tylko oszacowanie. Powinno to dotyczyć praktycznie każdego typu kolekcji - z wyjątkiem a LinkedList.

Jeśli to możliwe, używaj tablic strukturalnych (lub list)

Niewiele zyskujesz na stosowaniu struktur (lub ogólnie typów wartości), jeśli przenosisz je między obiektami. Na przykład w większości „dobrych” układów cząstek poszczególne cząstki są przechowywane w masywnym układzie: układ i wskaźnik są przekazywane wokół samej cząstki. Powodem, dla którego działa to tak dobrze, jest to, że gdy GC musi zebrać tablicę, może całkowicie pominąć zawartość (jest to prymitywna tablica - tutaj nie ma nic do zrobienia). Zamiast więc patrzeć na 10 000 obiektów, GC musi po prostu spojrzeć na 1 tablicę: ogromny zysk! Ponownie będzie to działać tylko z typami wartości .

Po RoyT. dostarczyła realnych i konstruktywnych informacji zwrotnych. Myślę, że muszę rozwinąć tę kwestię jeszcze bardziej. Tej techniki powinieneś używać tylko wtedy, gdy masz do czynienia z ogromną ilością bytów (tysiące do dziesiątek tysięcy). Ponadto musi to być struktura, która nie może zawierać żadnych pól typu odwołania i musi znajdować się w tablicy z wyraźnie określonym typem. W przeciwieństwie do jego opinii umieszczamy go w tablicy, która najprawdopodobniej jest polem w klasie - co oznacza, że ​​wyląduje na stosie (nie próbujemy uniknąć przydziału stosu - po prostu unikamy pracy GC). Naprawdę zależy nam na tym, aby był to ciągły fragment pamięci z wieloma wartościami, na które GC może po prostu spojrzeć w O(1)operacji zamiast O(n)operacji.

Powinieneś także przydzielić te tablice jak najbliżej uruchomienia aplikacji, aby zminimalizować ryzyko wystąpienia fragmentacji lub nadmiernej pracy, gdy GC próbuje przenieść te fragmenty (i rozważ użycie hybrydowej listy zamiast wbudowanego Listtypu ).

GC.Collect ()

To zdecydowanie NAJLEPSZY sposób na zastrzelenie się w stopę (patrz: „Rozważania na temat wydajności”) za pomocą pokoleniowego GC. Powinieneś wywoływać go tylko wtedy, gdy utworzyłeś EKSTREMALNĄ ilość śmieci - a jednym z przypadków, w którym może to być problem, jest tuż po załadowaniu zawartości dla poziomu - i nawet wtedy prawdopodobnie powinieneś zebrać tylko pierwszą generację ( GC.Collect(0);) miejmy nadzieję, że zapobiegną promowaniu obiektów do trzeciej generacji.

IDisposable i zerowanie pola

Warto zerować pola, gdy nie potrzebujesz już obiektu (bardziej w przypadku obiektów z ograniczeniami). Powodem są szczegóły działania GC: usuwa tylko obiekty, które nie są zrootowane (tj. Do których się odwołuje), nawet jeśli ten obiekt zostałby cofnięty z powodu usunięcia innych obiektów z bieżącej kolekcji ( uwaga: zależy to od GC smak w użyciu - niektóre faktycznie usuwają łańcuchy). Ponadto, jeśli obiekt przeżyje kolekcję, jest on natychmiast awansowany do następnej generacji - oznacza to, że wszelkie przedmioty pozostawione na polach zostaną awansowane podczas kolekcji. Każde kolejne pokolenie jest wykładniczo droższe w zbieraniu (i zdarza się rzadko).

Weź następujący przykład:

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

Jeśli MyFurtherNestedObjectzawiera obiekt o wielkości wielu megabajtów, możesz mieć pewność, że GC nie będzie na niego długo patrzeć - ponieważ przypadkowo awansowałeś go do G3. Porównaj to z tym przykładem:

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

Wzorzec Disposer pomaga skonfigurować przewidywalny sposób, w jaki obiekty proszą o wyczyszczenie ich prywatnych pól. Na przykład:

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
źródło
1
WTF, jesteś na temat? .NET Framework w systemie Windows jest całkowicie generacyjny. Ich klasa ma nawet metody GetGeneration .
DeadMG,
2
.NET w systemie Windows korzysta z Generational Garbage Collector, jednak .NET Compact Framework stosowany w WP7 i Xbox360 ma bardziej ograniczony GC, choć prawdopodobnie jest to coś więcej niż płaska lista, ale nie jest to pełna generacja.
Roy T.
Jonathan Dickinson: Chciałbym dodać, że struktury nie zawsze są odpowiedzią. W .Net, a więc prawdopodobnie także w trybie Mono, struct niekoniecznie musi istnieć na stosie (co jest dobre), ale może także żyć na stosie (gdzie potrzebujesz Garbage Collector). Reguły tego są dość skomplikowane, ale generalnie dzieje się tak, gdy struktura zawiera typy inne niż wartościowe.
Roy T.
1
@RoyT. patrz powyższy komentarz. Wskazałem, że struktury są dobre dla dużej ilości danych liniowych w macierzy - jak układ cząstek. Jeśli martwisz się strukturami GC w tablicy, to droga jest do zrobienia; dla danych takich jak cząstki.
Jonathan Dickinson
10
@DeadMG również WTF jest wysoce nieuzasadniony - biorąc pod uwagę, że tak naprawdę nie przeczytałeś poprawnie odpowiedzi. Uspokój się i przeczytaj ponownie odpowiedź przed napisaniem nienawistnych komentarzy w ogólnie dobrze wychowanej społeczności, takiej jak ta.
Jonathan Dickinson
7

Bardzo mała dynamiczna instancja występuje automatycznie w bibliotece podstawowej, chyba że wywołasz coś, co tego wymaga. Zdecydowana większość alokacji i dezalokacji pamięci pochodzi z własnego kodu i można to kontrolować.

Jak rozumiem, nie można tego całkowicie uniknąć - wszystko, co możesz zrobić, to zapewnić recykling i pula obiektów tam, gdzie to możliwe, używać struktur zamiast klas, unikać systemu UnityGUI i ogólnie unikać tworzenia nowych obiektów w pamięci dynamicznej w czasie gdy liczy się wydajność.

Możesz także wymusić wyrzucanie elementów bezużytecznych w określonych momentach, co może, ale nie musi pomóc: zobacz niektóre sugestie tutaj: http://unity3d.com/support/documentation/Manual/iphone-Optimizing-Scripts.html

Kylotan
źródło
2
Powinieneś naprawdę wyjaśnić implikacje wymuszenia GC. Odgrywa spustoszenie w heurystyce i układzie GC i może prowadzić do większych problemów z pamięcią, jeśli zostanie nieprawidłowo zastosowany: społeczność XNA / MVP / personel ogólnie uważa, że ​​ładowanie zawartości jest jedynym rozsądnym czasem na wymuszenie pobrania. Naprawdę powinieneś unikać wspominania o tym całkowicie, jeśli poświęcisz temu zagadnieniu tylko jedno zdanie. GC.Collect()= wolna pamięć teraz, ale bardzo prawdopodobne problemy później.
Jonathan Dickinson
Nie, powinieneś wyjaśnić implikacje wymuszenia GC, ponieważ wiesz o tym więcej niż ja. :) Należy jednak pamiętać, że Mono GC niekoniecznie jest taki sam jak Microsoft GC, a ryzyko może nie być takie samo.
Kylotan,
Nadal +1. Poszedłem dalej i rozwinąłem osobiste doświadczenia z GC - które mogą Cię zainteresować.
Jonathan Dickinson