Perché il ciclo for è più efficiente del ciclo for in Unity?


8

Durante un discorso di Unity sulle prestazioni, il ragazzo ci ha detto di evitare il for eachloop per aumentare le prestazioni e utilizzare invece il normale ciclo. Qual è la differenza tra fore for eachdal punto di vista dell'implementazione? Cosa rende for eachinefficiente?


1
Oltre a contare quanti elementi ci sono nell'array o qualunque cosa tu stia usando, non dovrebbe esserci molta differenza. Con una rapida ricerca su Google ho trovato questo: stackoverflow.com/questions/3430194/… (Questo vale anche per Unity, anche se la lingua è diversa, è sempre la stessa logica di base applicabile)
John Hamilton,

2
Aggiungerei: nella tua prima iterazione non ottimizzare le prestazioni ma una priorità dovrebbe essere un'architettura pulita e uno stile di codice. Ottimizza (prestazioni) solo se ce n'è bisogno in seguito ...
Marco

2
Hai un link a quel discorso?
Vaillancourt

2
Secondo questa pagina Unity , i for eachcicli che ripetono qualsiasi cosa diversa da un array generano immondizia (versioni di Unity precedenti alla 5.5)
Hellium,

2
Vedo un voto per chiuderlo come off-topic perché è una programmazione generale, ma dal momento che è richiesto nel contesto di Unity in particolare e questo è un comportamento che è cambiato tra le versioni di Unity, penso che valga la pena tenere aperto qui per riferimento di Sviluppatori di giochi Unity.
DMGregory

Risposte:


13

Un ciclo foreach apparentemente crea un oggetto IEnumerator fuori dalla raccolta in cui lo si passa e quindi lo attraversa. Quindi un ciclo come questo:

foreach(var entry in collection)
{
    entry.doThing();
}

si traduce in qualcosa di simile a questo:
(elimina un po 'di complessità riguardo al modo in cui l'enumeratore viene eliminato in seguito)

var enumerator = collection.GetEnumerator();
while(enumerator.MoveNext()) {
   var entry = enumerator.Current;
   entry.doThing();
}

Se questo viene compilato ingenuamente / direttamente, l'oggetto enumeratore viene allocato sull'heap (che richiede un po 'di tempo), anche se il suo tipo di base è una struttura - qualcosa chiamato boxe. Dopo che il ciclo è terminato, questa allocazione diventa spazzatura da ripulire in seguito e il gioco può balbettare per un momento se un sacco di spazzatura si accumula per consentire al raccoglitore di eseguire una scansione completa. Infine, il MoveNextmetodo e la Currentproprietà potrebbero comportare più passaggi di chiamata delle funzioni rispetto alla semplice acquisizione di un elemento direttamente collection[i], se il compilatore non incorpora tale azione.

I documenti Unity di Hellium sopra riportati indicano che questa forma ingenua è esattamente ciò che accade in Unity prima della versione 5.5 , se scorre su qualcosa di diverso da una matrice nativa.

Nella versione 5.6+ (comprese le versioni 2017), questa boxe non necessaria è sparita e non dovresti riscontrare allocazioni non necessarie se stai usando un tipo concreto (ad es. int[]Oppure List<GameObject>oppure Queue<T>).

Otterrai comunque un'allocazione se il metodo in questione sa solo che sta funzionando con un qualche tipo di IList o altra interfaccia - in quei casi non sa con quale specifico enumeratore sta lavorando quindi deve prima inserirlo in un oggetto IEnumerator .

Quindi, c'è una buona possibilità che si tratti di vecchi consigli che non sono così importanti nello sviluppo moderno di Unity. Gli sviluppatori di unità come noi possono essere un po 'superstiziosi su cose che un tempo erano lente / pelose nelle vecchie versioni, quindi prendi qualsiasi consiglio di quella forma con un granello di sale. ;) Il motore si evolve più velocemente delle nostre convinzioni al riguardo.

In pratica, non ho mai avuto un gioco che mostrasse una notevole lentezza o singhiozzi di raccolta dei rifiuti dall'uso di cicli foreach, ma il tuo chilometraggio può variare. Crea il profilo del tuo gioco sull'hardware scelto per vedere se vale la pena preoccuparsi. Se è necessario, è piuttosto banale sostituire i loop foreach con espliciti per loop in seguito allo sviluppo, qualora si manifestasse un problema, quindi non sacrificherei la leggibilità e la facilità di codifica all'inizio dello sviluppo.


Solo per verificare, a List<MyCustomType>e IEnumerator<MyCustomType> getListEnumerator() { }vanno bene, mentre un metodo IEnumerator getListEnumerator() { }non sarebbe.
Draco18s non si fida più di SE il

0

Se a per ogni ciclo utilizzato per la raccolta o la matrice di oggetti (ovvero la matrice di tutti gli elementi diversi dal tipo di dati primitivo), GC (Garbage Collector) viene chiamato per liberare spazio della variabile di riferimento alla fine di a per ciascun ciclo.

foreach (Gameobject temp in Collection)
{
    //Code Do Task
}

Considerando che il ciclo for viene usato per iterare sugli elementi usando un indice e quindi il tipo di dati primitivo non influirà sulle prestazioni rispetto a un tipo di dati non primitivo.

for (int i = 0; i < Collection.length; i++){
    //Get reference using index i;
    //Code Do Task
}

Hai una citazione per questo? La tempvariabile qui può essere allocata nello stack, anche se la raccolta è piena di tipi di riferimento (poiché è solo un riferimento alle voci esistenti già sull'heap, non allocare una nuova memoria heap per contenere un'istanza di quel tipo di riferimento), quindi non ci aspetteremmo che si verifichino rifiuti qui. L'enumeratore stesso può essere inscatolato in alcune circostanze e finire nell'heap, ma questi casi si applicano anche ai tipi di dati primitivi.
DMGregory
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.