Come devo tenere conto del GC durante la creazione di giochi con Unity?


15

* Per quanto ne so, Unity3D per iOS si basa sul runtime Mono e Mono ha solo GC generazionale mark & ​​sweep.

Questo sistema GC non può evitare il tempo GC che arresta il sistema di gioco. Il pool di istanze può ridurlo, ma non completamente, perché non possiamo controllare l'istanza nella libreria di classi base del CLR. Questi casi nascosti piccoli e frequenti aumenteranno il tempo GC non deterministico alla fine. Forzare periodicamente GC completo degraderà notevolmente le prestazioni (in realtà Mono può forzare GC completo?)

Quindi, come posso evitare questo tempo GC quando uso Unity3D senza un notevole peggioramento delle prestazioni?


3
L'ultimo Unity Pro Profiler include la quantità di immondizia generata in determinate chiamate di funzione. È possibile utilizzare questo strumento per aiutare a vedere altri luoghi che potrebbero generare rifiuti che altrimenti non sarebbero immediatamente ovvi.
Tetrad

Risposte:


18

Fortunatamente, come hai sottolineato, le build COMPACT Mono utilizzano un GC generazionale (in netto contrasto con quelle di Microsoft, come WinMo / WinPhone / XBox, che mantengono solo un elenco semplice).

Se il tuo gioco è semplice, il GC dovrebbe gestirlo bene, ma ecco alcuni suggerimenti che potresti voler esaminare.

Ottimizzazione precoce

Prima di tutto assicurati che questo sia effettivamente un problema per te prima di provare a risolverlo.

Raggruppamento di tipi di riferimento costosi

Dovresti raggruppare i tipi di riferimento che crei spesso o che hanno strutture profonde. Un esempio di ciascuno sarebbe:

  • Creato spesso: un Bulletoggetto in un gioco infernale .
  • Struttura profonda: albero decisionale per un'implementazione dell'IA.

Dovresti usare a Stackcome pool (diversamente dalla maggior parte delle implementazioni che usano a Queue). Il motivo è perché con un Stackse si restituisce un oggetto al pool e qualcos'altro lo afferra immediatamente; avrà una probabilità molto maggiore di trovarsi in una pagina attiva - o anche nella cache della CPU se sei fortunato. È solo un po 'più veloce. Inoltre, limita sempre le dimensioni dei tuoi pool (ignora semplicemente i "checkin" se il tuo limite è stato superato).

Evita di creare nuove liste per cancellarle

Non crearne uno nuovo Listquando lo intendevi davvero Clear(). È possibile riutilizzare l'array back-end e salvare un carico di allocazioni e copie dell'array. Analogamente a questo, prova a creare elenchi con una capacità iniziale significativa (ricorda, questo non è un limite - solo una capacità iniziale) - non deve essere preciso, solo una stima. Ciò dovrebbe applicarsi praticamente a qualsiasi tipo di raccolta, ad eccezione di a LinkedList.

Usa gli array (o gli elenchi) Struct ove possibile

Si ottengono pochi benefici dall'uso delle strutture (o dei tipi di valore in generale) se le si passa tra gli oggetti. Ad esempio, nella maggior parte dei sistemi di particelle "buoni" le singole particelle sono immagazzinate in un array enorme: l'array e l'indice vengono fatti passare al posto della particella stessa. Il motivo per cui funziona così bene è perché quando il GC ha bisogno di raccogliere l'array, può saltare completamente il contenuto (è un array primitivo - niente da fare qui). Quindi, invece di guardare 10.000 oggetti, il GC deve semplicemente guardare 1 array: enorme guadagno! Ancora una volta, questo funzionerà solo con tipi di valore .

Dopo RoyT. ha fornito alcuni riscontri fattibili e costruttivi che ritengo di dover approfondire ulteriormente. Dovresti usare questa tecnica solo quando hai a che fare con enormi quantità di entità (da migliaia a decine di migliaia). Inoltre, deve essere una struttura che non deve avere campi del tipo di riferimento e deve risiedere in una matrice tipizzata in modo esplicito. Contrariamente al suo feedback, lo stiamo posizionando in un array che è molto probabilmente un campo in una classe - il che significa che sta per atterrare sull'heap (non stiamo cercando di evitare un'allocazione dell'heap - semplicemente evitando il lavoro GC). Ci interessa davvero il fatto che si tratti di un pezzo contiguo di memoria con molti valori che il GC può semplicemente guardare in O(1)un'operazione anziché in O(n)un'operazione.

È inoltre necessario allocare questi array il più vicino possibile all'avvio dell'applicazione per mitigare le possibilità di frammentazione o eccessivo lavoro mentre il GC tenta di spostare questi blocchi (e considerare l'utilizzo di un elenco ibrido collegato invece del Listtipo incorporato ).

GC.Collect ()

Questo è sicuramente il modo MIGLIORE per spararti al piede (vedi: "Considerazioni sulle prestazioni") con un GC generazionale. Dovresti chiamarlo solo quando hai creato una quantità ESTREMA di immondizia - e l'unica istanza in cui ciò potrebbe costituire un problema è subito dopo aver caricato il contenuto per un livello - e anche allora probabilmente dovresti raccogliere solo la prima generazione ( GC.Collect(0);) speriamo di prevenire la promozione di oggetti alla terza generazione.

IDisposable e Field Nulling

Vale la pena annullare i campi quando non è più necessario un oggetto (più o meno su oggetti vincolati). Il motivo è nei dettagli di come funziona il GC: rimuove solo oggetti che non sono rooted (cioè referenziati) anche se quell'oggetto sarebbe stato sradicato a causa della rimozione di altri oggetti nella raccolta corrente ( nota: questo dipende dal GC sapore in uso - alcuni effettivamente puliscono le catene). Inoltre, se un oggetto sopravvive a una raccolta, viene immediatamente promosso alla generazione successiva, ciò significa che qualsiasi oggetto lasciato in giro nei campi verrà promosso durante una raccolta. Ogni generazione successiva è esponenzialmente più costosa da collezionare (e si presenta raramente).

Prendi il seguente esempio:

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

Se MyFurtherNestedObjectconteneva un oggetto multi-megabyte, puoi essere certo che il GC non lo guarderà per molto tempo, perché lo hai inavvertitamente promosso a G3. Contrastalo con questo esempio:

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

Il modello di eliminazione consente di impostare un modo prevedibile per chiedere agli oggetti di cancellare i loro campi privati. Per esempio:

public class MyClass : IDisposable
{
    private MyNestedType _nested;

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

    public void Dispose()
    {
       _nested = null;
    }
}

1
A proposito di cosa stai parlando? .NET Framework su Windows è assolutamente generazionale. La loro classe ha persino metodi GetGeneration .
DeadMG

2
.NET su Windows utilizza un Garbage Collector generazionale, tuttavia .NET Compact Framework utilizzato su WP7 e Xbox360 ha un GC più limitato, anche se probabilmente è più di un elenco semplice non è un generazionale completo.
Roy T.

Jonathan Dickinson: Vorrei aggiungere che le strutture non sono sempre la risposta. In .Net e quindi probabilmente anche Mono una struct non vive necessariamente nello stack (il che è buono) ma può anche vivere nell'heap (che è dove è necessario un Garbage Collector). Le regole per questo sono abbastanza complicate, ma in generale succede quando uno struct contiene tipi non di valore.
Roy T.

1
@RoyT. vedi il commento sopra. Ho indicato che le strutture sono buone per una grande quantità di dati in un array - come un sistema di particelle. Se sei preoccupato per le strutture GC in un array, è la strada da percorrere; per dati come particelle.
Jonathan Dickinson,

10
@DeadMG anche WTF è altamente richiesto - considerando che in realtà non hai letto correttamente la risposta. Per favore, calmati e rileggi la risposta prima di scrivere commenti odiosi in una comunità generalmente ben educata come questa.
Jonathan Dickinson,

7

L'istanza dinamica molto piccola si verifica automaticamente all'interno della libreria di base, a meno che non si chiami qualcosa che lo richiede. La stragrande maggioranza dell'allocazione e della deallocazione della memoria proviene dal tuo codice e puoi controllarlo.

A quanto ho capito, non puoi evitarlo del tutto: tutto ciò che puoi fare è assicurarti di riciclare e raggruppare gli oggetti dove possibile, utilizzare le strutture anziché le classi, evitare il sistema UnityGUI e in genere evitare di creare nuovi oggetti nella memoria dinamica durante i periodi quando le prestazioni contano.

È inoltre possibile forzare la raccolta dei rifiuti in determinati momenti, il che può o meno aiutare: vedere alcuni dei suggerimenti qui: http://unity3d.com/support/documentation/Manual/iphone-Optimizing-Scripts.html


2
Dovresti davvero spiegare le implicazioni del forzare GC. Gioca nel caos con l'euristica e il layout del GC e può portare a maggiori problemi di memoria se usato in modo errato: la comunità XNA / MVPs / staff generalmente considera il post-caricamento-contenuto l'unico tempo ragionevole per forzare una raccolta. Dovresti davvero evitare di menzionarlo interamente se dedichi solo una frase all'argomento. GC.Collect()= memoria libera ora ma problemi molto probabili in seguito.
Jonathan Dickinson,

No, dovresti spiegare le implicazioni del forzare GC perché ne conosci più di me. :) Tuttavia, tieni presente che Mono GC non è necessariamente uguale a Microsoft GC e che i rischi potrebbero non essere gli stessi.
Kylotan,

Ancora, +1. Sono andato avanti e ho approfondito l'esperienza personale con il GC - che potresti trovare interessante.
Jonathan Dickinson,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.