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
Bullet
oggetto in un gioco infernale .
- Struttura profonda: albero decisionale per un'implementazione dell'IA.
Dovresti usare a Stack
come pool (diversamente dalla maggior parte delle implementazioni che usano a Queue
). Il motivo è perché con un Stack
se 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 List
quando 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 List
tipo 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 MyFurtherNestedObject
conteneva 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;
}
}