Comment dois-je prendre en compte le GC lors de la création de jeux avec Unity?

15

* Pour autant que je sache, Unity3D pour iOS est basé sur le runtime Mono et Mono n'a que GC génération et balayage de génération.

Ce système GC ne peut pas éviter le temps GC qui arrête le système de jeu. Le regroupement d'instances peut réduire cela mais pas complètement, car nous ne pouvons pas contrôler l'instanciation qui se produit dans la bibliothèque de classes de base du CLR. Ces petites instances fréquentes et cachées augmenteront éventuellement le temps GC non déterministe. Forcer périodiquement un GC complet dégradera considérablement les performances (Mono peut-il forcer un GC complet, en fait?)

Alors, comment puis-je éviter ce temps GC lors de l'utilisation de Unity3D sans dégrader les performances?

Eonil
la source
3
Le dernier Unity Pro Profiler inclut la quantité de déchets générés dans certains appels de fonction. Vous pouvez utiliser cet outil pour voir d'autres endroits susceptibles de générer des ordures qui ne seraient pas immédiatement évidents autrement.
Tetrad

Réponses:

18

Heureusement, comme vous l'avez souligné, les versions COMPACT Mono utilisent un GC générationnel (contrairement à celles de Microsoft, comme WinMo / WinPhone / XBox, qui ne font que maintenir une liste plate).

Si votre jeu est simple, le GC devrait le gérer très bien, mais voici quelques conseils que vous voudrez peut-être examiner.

Optimisation prématurée

Assurez-vous d'abord que c'est réellement un problème avant d'essayer de le résoudre.

Mise en commun des types de référence coûteux

Vous devez regrouper les types de référence que vous créez souvent ou qui ont des structures profondes. Un exemple de chacun serait:

  • Créé souvent: un Bulletobjet dans un jeu d'enfer .
  • Structure profonde: Arbre de décision pour une implémentation de l'IA.

Vous devez utiliser a Stackcomme pool (contrairement à la plupart des implémentations qui utilisent a Queue). La raison en est qu'avec un Stacksi vous renvoyez un objet dans le pool et quelque chose d'autre le saisit immédiatement; il aura beaucoup plus de chances d'être dans une page active - ou même dans le cache CPU si vous êtes chanceux. C'est juste un tout petit peu plus rapide. De plus, limitez toujours la taille de vos pools (ignorez simplement les «enregistrements» si votre limite a été dépassée).

Évitez de créer de nouvelles listes pour les effacer

Ne créez pas de nouveau Listquand vous le vouliez réellement Clear(). Vous pouvez réutiliser le tableau principal et enregistrer une charge d'allocations et de copies de tableau. De la même manière, essayez de créer des listes avec une capacité initiale significative (rappelez-vous, ce n'est pas une limite - juste une capacité de départ) - cela n'a pas besoin d'être précis, juste une estimation. Cela devrait s'appliquer à pratiquement n'importe quel type de collection - à l'exception de a LinkedList.

Utilisez des tableaux (ou des listes) de structures dans la mesure du possible

Vous gagnez peu d'avantages à utiliser des structures (ou des types de valeurs en général) si vous les passez entre les objets. Par exemple, dans la plupart des «bons» systèmes de particules, les particules individuelles sont stockées dans un réseau massif: le réseau et l'indice sont transmis à la place de la particule elle-même. La raison pour laquelle cela fonctionne si bien est que lorsque le GC doit collecter le tableau, il peut ignorer le contenu entièrement (c'est un tableau primitif - rien à faire ici). Ainsi, au lieu de regarder 10 000 objets, le GC doit simplement regarder 1 tableau: un gain énorme! Encore une fois, cela ne fonctionnera qu'avec les types de valeur .

Après RoyT. a fourni des commentaires viables et constructifs, je pense que je dois développer davantage. Vous ne devez utiliser cette technique que lorsque vous avez affaire à des quantités massives d'entités (des milliers à des dizaines de milliers). De plus, ce doit être une structure qui ne doit pas avoir de champs de type référence et doit vivre dans un tableau explicitement typé. Contrairement à ses commentaires, nous le plaçons dans un tableau qui est très probablement un champ dans une classe - ce qui signifie qu'il va atterrir le tas (nous n'essayons pas d'éviter une allocation de tas - simplement en évitant le travail GC). Nous nous soucions vraiment du fait qu'il s'agit d'un bloc de mémoire contigu avec beaucoup de valeurs que le GC peut simplement regarder dans une O(1)opération au lieu d'une O(n)opération.

Vous devez également allouer ces tableaux le plus près possible du démarrage de votre application pour atténuer les risques de fragmentation ou de travail excessif lorsque le GC essaie de déplacer ces morceaux (et envisagez d'utiliser une liste liée hybride au lieu du Listtype intégré ). ).

GC.Collect ()

C'est définitivement LA MEILLEURE façon de vous tirer une balle dans le pied (voir: "Considérations de performances") avec un GC générationnel. Vous ne devriez l'appeler que lorsque vous avez créé une quantité EXTRÊME de déchets - et la seule instance où cela pourrait être un problème est juste après avoir chargé le contenu pour un niveau - et même alors, vous ne devriez probablement collecter que la première génération ( GC.Collect(0);) avec un peu de chance pour empêcher la promotion d'objets à la troisième génération.

IDisposable et Field Nulling

Il vaut la peine d'annuler les champs lorsque vous n'avez plus besoin d'un objet (plus encore sur les objets contraints). La raison en est dans les détails du fonctionnement du GC: il supprime uniquement les objets qui ne sont pas enracinés (c.-à-d. Référencés) même si cet objet aurait été non raciné en raison du retrait d'autres objets dans la collection actuelle ( remarque: cela dépend du GC saveur utilisée - certains nettoient les chaînes). De plus, si un objet survit à une collection, il est immédiatement promu à la génération suivante - cela signifie que tout objet laissé traîner dans les champs sera promu lors d'une collection. Chaque génération successive est exponentiellement plus coûteuse à collecter (et se produit aussi rarement).

Prenons l'exemple suivant:

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

S'il MyFurtherNestedObjectcontient un objet de plusieurs mégaoctets, vous pouvez être assuré que le GC ne le regardera pas assez longtemps - parce que vous l'avez par inadvertance promu en G3. Comparez cela avec cet exemple:

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

Le modèle Disposer vous aide à configurer un moyen prévisible de demander aux objets d'effacer leurs champs privés. Par exemple:

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
la source
1
Vous êtes sur WTF? Le .NET Framework sur Windows est absolument générationnel. Leur classe a même des méthodes GetGeneration .
DeadMG
2
.NET sur Windows utilise un Garbage Collector générationnel, mais le .NET Compact Framework tel qu'utilisé sur WP7 et la Xbox360 a un GC plus limité, bien qu'il s'agisse probablement plus d'une liste plate qu'il ne s'agit pas d'une génération complète.
Roy T.
Jonathan Dickinson: Je voudrais ajouter que les structures ne sont pas toujours la réponse. Dans .Net et donc probablement aussi Mono, une structure ne vit pas nécessairement sur la pile (ce qui est bien) mais peut également vivre sur le tas (c'est là que vous avez besoin d'un Garbage Collector). Les règles pour cela sont assez compliquées mais en général cela se produit quand une structure contient des types sans valeur.
Roy T.
1
@RoyT. voir le commentaire ci-dessus. J'ai indiqué que les structures sont bonnes pour une grande quantité de données dans un tableau - comme un système de particules. Si vous êtes inquiet au sujet des structures GC dans un tableau, c'est la voie à suivre; pour des données comme des particules.
Jonathan Dickinson
10
@DeadMG également WTF est très inutile - étant donné que vous n'avez pas lu correctement la réponse. Veuillez calmer votre humeur et relire la réponse avant d'écrire des commentaires haineux dans une communauté généralement bien tenue comme celle-ci.
Jonathan Dickinson
7

Très peu d'instanciation dynamique se produit automatiquement dans la bibliothèque de base, sauf si vous appelez quelque chose qui l'exige. La grande majorité de l'allocation et de la désallocation de mémoire provient de votre propre code et vous pouvez contrôler cela.

Si je comprends bien, vous ne pouvez pas l'éviter complètement - tout ce que vous pouvez faire est de vous assurer que vous recyclez et regroupez les objets lorsque cela est possible, utilisez des structures au lieu de classes, évitez le système UnityGUI et évitez généralement de créer de nouveaux objets dans la mémoire dynamique pendant les périodes quand la performance compte.

Vous pouvez également forcer la collecte des ordures à certains moments, ce qui peut ou non vous aider: voir certaines des suggestions ici: http://unity3d.com/support/documentation/Manual/iphone-Optimizing-Scripts.html

Kylotan
la source
2
Vous devez vraiment expliquer les implications du forçage de GC. Il fait des ravages avec l'heuristique et la disposition du GC et peut entraîner de plus gros problèmes de mémoire s'il est utilisé de manière incorrecte: la communauté XNA / MVP / staff considère généralement le post-chargement du contenu comme le seul temps raisonnable pour forcer une collecte. Vous devriez vraiment éviter de le mentionner entièrement si vous ne consacrez qu'une phrase à la question. GC.Collect()= mémoire libre maintenant mais problèmes très probables plus tard.
Jonathan Dickinson
Non, vous devez expliquer les implications du forçage de GC parce que vous en savez plus que moi. :) Cependant, gardez à l'esprit que le Mono GC n'est pas nécessairement le même que le Microsoft GC, et les risques peuvent ne pas être les mêmes.
Kylotan
Pourtant, +1. Je suis allé de l'avant et j'ai développé une expérience personnelle avec le GC - que vous pourriez trouver intéressante.
Jonathan Dickinson