¿Cómo debo tener en cuenta el GC al crear juegos con Unity?

15

* Hasta donde sé, Unity3D para iOS se basa en el tiempo de ejecución de Mono y Mono solo tiene GC generacional de marca y barrido.

Este sistema GC no puede evitar el tiempo GC que detiene el sistema del juego. La agrupación de instancias puede reducir esto, pero no por completo, porque no podemos controlar la instanciación que ocurre en la biblioteca de clases base del CLR. Esas instancias pequeñas y frecuentes ocultas eventualmente aumentarán el tiempo de GC no determinista. Forzar un GC completo periódicamente degradará enormemente el rendimiento (¿puede Mono forzar un GC completo, en realidad?

Entonces, ¿cómo puedo evitar este tiempo de GC cuando uso Unity3D sin una gran degradación del rendimiento?

Eonil
fuente
3
El último Unity Pro Profiler incluye la cantidad de basura generada en ciertas llamadas de función. Puede usar esta herramienta para ayudar a ver otros lugares que podrían estar generando basura que de lo contrario no serían inmediatamente obvios.
Tetrad

Respuestas:

18

Afortunadamente, como usted señaló, el COMPACTO compilaciones Mono usan un GC generacional (en marcado contraste con los de Microsoft, como WinMo / WinPhone / XBox, que solo mantienen una lista plana).

Si su juego es simple, el GC debería manejarlo bien, pero aquí hay algunos consejos que puede considerar.

Optimización prematura

Primero asegúrese de que esto sea realmente un problema para usted antes de intentar solucionarlo.

Agrupación de tipos de referencia caros

Debe agrupar los tipos de referencia que crea con frecuencia o que tienen estructuras profundas. Un ejemplo de cada uno sería:

  • Creado a menudo: Un Bulletobjeto en un juego de infierno de balas .
  • Estructura profunda: árbol de decisión para una implementación de IA.

Debe usar a Stackcomo su grupo (a diferencia de la mayoría de las implementaciones que usan a Queue). La razón de esto es porque con unStack si devuelve un objeto al grupo y algo más lo agarra inmediatamente; Tendrá muchas más posibilidades de estar en una página activa, o incluso en el caché de la CPU si tiene suerte. Es un poquito más rápido. Además, siempre limite el tamaño de sus piscinas (solo ignore los 'registros' si se ha excedido su límite).

Evite crear nuevas listas para borrarlas

No cree uno nuevo Listcuando realmente lo haya querido Clear(). Puede reutilizar la matriz de back-end y guardar una gran cantidad de asignaciones y copias de la matriz. De manera similar a este, intente y cree listas con una capacidad inicial significativa (recuerde, esto no es un límite, solo una capacidad inicial), no necesita ser precisa, solo una estimación. Esto debería aplicarse básicamente a cualquier tipo de colección, excepto a LinkedList.

Utilice matrices de estructura (o listas) donde sea posible

Obtiene pocos beneficios del uso de estructuras (o tipos de valor en general) si los pasa entre objetos. Por ejemplo, en la mayoría de los sistemas de partículas "buenas", las partículas individuales se almacenan en una matriz masiva: la matriz y el índice se pasan en lugar de la partícula misma. La razón por la que esto funciona tan bien es porque cuando el GC necesita recopilar la matriz, puede omitir el contenido por completo (es una matriz primitiva, no hay nada que hacer aquí). Entonces, en lugar de mirar 10 000 objetos, el GC simplemente necesita mirar 1 matriz: ¡gran ganancia! Nuevamente, esto solo funcionará con tipos de valor .

Después de RoyT. proporcionó algunos comentarios viables y constructivos. Siento que necesito ampliar más esto. Solo debe usar esta técnica cuando se trata de grandes cantidades de entidades (de miles a decenas de miles). Además, debe ser una estructura que no debe tener ningún campo de tipo de referencia y debe vivir en una matriz tipada explícitamente. Contrariamente a sus comentarios, lo estamos colocando en una matriz que es muy probable que sea un campo en una clase, lo que significa que va a aterrizar el montón (no estamos tratando de evitar una asignación de montón, simplemente evitando el trabajo de GC). Lo que realmente nos importa es el hecho de que es una porción contigua de memoria con muchos valores que el GC simplemente puede ver en una O(1)operación en lugar de una O(n)operación.

También debe asignar estos arreglos lo más cerca posible al inicio de su aplicación para mitigar las posibilidades de que ocurra fragmentación o trabajo excesivo a medida que el GC intenta mover estos fragmentos (y considere usar una lista enlazada híbrida en lugar del Listtipo incorporado) )

GC.Collect ()

Esta es definitivamente LA MEJOR forma de dispararse en el pie (ver: "Consideraciones de rendimiento") con un GC generacional. Solo debe llamarlo cuando haya creado una cantidad EXTREMA de basura, y la única instancia en la que eso podría ser un problema es justo después de haber cargado el contenido para un nivel, e incluso entonces probablemente solo deba recolectar la primera generación ( GC.Collect(0);) con suerte para evitar la promoción de objetos a la tercera generación.

IDisposable y anulación de campo

Vale la pena anular los campos cuando ya no necesita un objeto (más aún en objetos restringidos). La razón está en los detalles de cómo funciona el GC: solo elimina objetos que no están enraizados (es decir, referenciados) incluso si ese objeto se hubiera desrooteado debido a que otros objetos se eliminaron en la colección actual ( nota: esto depende del GC sabor en uso - algunos realmente limpian las cadenas). Además, si un objeto sobrevive a una colección, se promociona inmediatamente a la próxima generación; esto significa que cualquier objeto que quede en los campos se promocionará durante una colección. Cada generación sucesiva es exponencialmente más costosa de recolectar (y ocurre con poca frecuencia).

Tome el siguiente ejemplo:

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

Si MyFurtherNestedObjectcontiene un objeto de varios megabytes, puede estar seguro de que el GC no lo mirará durante mucho tiempo, porque lo ha promovido inadvertidamente a G3. Contrasta eso con este ejemplo:

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

El patrón Disposer te ayuda a configurar una forma predecible de pedirle a los objetos que borren sus campos privados. Por ejemplo:

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
fuente
1
¿Sobre qué estás hablando? .NET Framework en Windows es absolutamente generacional. Su clase incluso tiene métodos GetGeneration .
DeadMG
2
.NET en Windows usa un recolector de basura generacional, sin embargo, .NET Compact Framework como se usa en WP7 y Xbox360 tienen un GC más limitado, aunque probablemente sea más que una lista plana, no es una generación completa.
Roy T.
Jonathan Dickinson: Me gustaría agregar que las estructuras no siempre son la respuesta. En .Net y probablemente también Mono, una estructura no necesariamente vive en la pila (lo cual es bueno) sino que también puede vivir en el montón (que es donde necesita un recolector de basura). Las reglas para esto son bastante complicadas, pero en general sucede cuando una estructura contiene tipos sin valor.
Roy T.
1
@RoyT. Ver comentario anterior. Indiqué que las estructuras son buenas para una gran cantidad de datos de una matriz, como un sistema de partículas. Si le preocupan las estructuras GC en una matriz, este es el camino a seguir; para datos como partículas.
Jonathan Dickinson el
10
@DeadMG también WTF es altamente innecesario, teniendo en cuenta que en realidad no leyó la respuesta correctamente. Calma tu temperamento y vuelve a leer la respuesta antes de escribir comentarios de odio en una comunidad en general de buen comportamiento como esta.
Jonathan Dickinson
7

Muy poca instanciación dinámica ocurre automáticamente dentro de la biblioteca base, a menos que llame a algo que lo requiera. La gran mayoría de la asignación de memoria y desasignación proviene de su propio código y puede controlarlo.

Según tengo entendido, no puede evitar esto por completo: todo lo que puede hacer es asegurarse de reciclar y agrupar objetos donde sea posible, usar estructuras en lugar de clases, evitar el sistema UnityGUI y, en general, evitar crear nuevos objetos en la memoria dinámica durante los tiempos cuando el rendimiento importa.

También puede forzar la recolección de basura en ciertos momentos, lo que puede o no ayudar: consulte algunas de las sugerencias aquí: http://unity3d.com/support/documentation/Manual/iphone-Optimizing-Scripts.html

Kylotan
fuente
2
Realmente deberías explicar las implicaciones de forzar GC. Causa estragos con la heurística y el diseño del GC y puede generar problemas de memoria mayores si se usa incorrectamente: la comunidad XNA / MVP / personal generalmente considera que el contenido posterior a la carga es el único momento razonable para forzar una recopilación. Realmente debería evitar mencionarlo por completo si solo dedica una oración al asunto. GC.Collect()= memoria libre ahora pero muy probablemente problemas después.
Jonathan Dickinson el
No, deberías explicar las implicaciones de forzar GC porque sabes más sobre esto que yo. :) Sin embargo, tenga en cuenta que el Mono GC no es necesariamente el mismo que el GC de Microsoft, y los riesgos pueden no ser los mismos.
Kylotan
Aún así, +1. Seguí adelante y elaboré sobre alguna experiencia personal con el GC, que puede resultarle interesante.
Jonathan Dickinson el