De ce este bucla for mai eficientă decât bucla for each în Unity? (Dezvoltarea jocurilor, Unitate, Performanță)

Emad a intrebat.
a intrebat.

În timpul unei discuții despre performanță în Unity, tipul ne-a spus să evităm for each pentru a crește performanța și să folosim în schimb bucla for normală. Care este diferența dintre for și for each din punct de vedere al implementării? Ce face ca for each ineficientă?

Comentarii

  • În afară de a număra câte elemente sunt în matrice sau orice altceva folosiți, nu ar trebui să existe o mare diferență. Cu o căutare rapidă pe Google am găsit acest lucru: stackoverflow.com/questions/3430194/… (Acest lucru se va aplica și la Unity, chiar dacă limbajul este diferit, se aplică aceeași logică de bază).  > Por John Hamilton.
  • Aș adăuga: În prima iterație nu reglați fin performanța, ci o prioritate ar trebui să fie o arhitectură și un stil de cod curat. Optimizați (performanța) doar dacă este nevoie de ea mai târziu…-  > Por Marco.
  • Aveți un link către această discuție?  > Por Vaillancourt.
  • Conform acestui document pagina Unity, for each buclele care itersează peste orice altceva decât un array generează gunoi (versiuni Unity anterioare la 5.5) –  > Por Hellium.
  • Văd un vot pentru a închide acest lucru ca fiind în afara subiectului, deoarece este vorba de programare generală, dar din moment ce este întrebat în contextul Unity în special și acesta este un comportament care s-a schimbat între versiunile de Unity, cred că merită să rămână deschis aici pentru referință pentru dezvoltatorii de jocuri Unity.-  > Por DMGregory.
2 răspunsuri
DMGregory

O buclă foreach creează aparent un obiect IEnumerator din colecția pe care o treceți și apoi merge peste asta. Deci, o buclă ca aceasta:

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

se traduce în ceva de genul acesta:
(eludând o anumită complexitate în legătură cu modul în care enumeratorul este eliminat ulterior)

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

Dacă acest lucru este compilat în mod naiv/direct, atunci obiectul enumerator este alocat pe heap (ceea ce durează puțin timp), chiar dacă tipul său de bază este un struct – ceea ce se numește boxing. După ce bucla este terminată, această alocare devine gunoi care trebuie curățat ulterior, iar jocul se poate bâlbâi pentru un moment dacă se adună suficient gunoi pentru ca colectorul să efectueze o verificare completă. În cele din urmă, funcția MoveNext și Current ar putea implica mai multe etape de apelare a funcțiilor decât simpla preluare directă a unui element cu collection[i]dacă compilatorul nu include această acțiune în linie.

Documentele Unity pe care Hellium leagă mai sus indică faptul că această formă naivă este exact ceea ce se întâmplă în Unity înainte de versiunea 5.5, dacă iterați peste orice altceva decât un Array nativ.

În versiunea 5.6+ (inclusiv versiunile 2017), această boxare inutilă a dispărut, și nu ar trebui să experimentați alocarea inutilă dacă utilizați orice tip concret (de ex. int[] sau List<GameObject> sau Queue<T>).

Veți primi în continuare o alocare dacă metoda în cauză știe doar că lucrează cu un anumit tip de IList sau de altă interfață – în aceste cazuri, nu știe cu ce enumerator specific lucrează, așa că trebuie să îl transforme mai întâi într-un obiect IEnumerator.

Așadar, există o bună șansă ca acesta să fie un sfat vechi care nu mai este atât de important în dezvoltarea modernă a Unity. Dezvoltatorii Unity ca noi pot fi un pic superstițioși în legătură cu lucruri care folosit să fie lente / greoaie în versiunile vechi, așa că luați orice sfat de această formă cu un bob de sare. 😉 Motorul evoluează mai repede decât credințele noastre despre el.

În practică, nu am avut niciodată un joc care să prezinte o încetineală notabilă sau sughițuri de colectare a gunoiului din cauza utilizării buclelor foreach, dar kilometrajul dvs. poate varia. Faceți un profil al jocului dvs. pe hardware-ul ales pentru a vedea dacă merită să vă faceți griji. Dacă este necesar, este destul de banal să înlocuiți buclele foreach cu bucle for explicite mai târziu în dezvoltare, în cazul în care se manifestă o problemă, așa că eu nu aș sacrifica lizibilitatea și ușurința de codare la începutul dezvoltării.

Comentarii

  • Doar pentru a verifica, un List<MyCustomType> și IEnumerator<MyCustomType> getListEnumerator() { } sunt în regulă, în timp ce o metodă IEnumerator getListEnumerator() { } nu ar fi.  > Por Draco18s nu mai are încredere în SE.
Tejas Jasani

Dacă o buclă for each loop este utilizată pentru colectarea sau matrice de obiecte (adică matrice de toate elementele, altele decât cele de tip primitiv), GC (Garbage Collector) este apelat pentru a elibera spațiul variabilei de referință la sfârșitul unei bucle for each loop.

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

În timp ce bucla for este utilizată pentru a parcurge elementele folosind un index și, prin urmare, tipul de date primitiv nu va afecta performanța în comparație cu un tip de date neprimitiv.

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

Comentarii

  • Aveți o citare pentru acest lucru? temp poate fi alocată pe stivă, chiar dacă colecția este plină de tipuri de referință (deoarece este doar o referință la intrările existente deja pe heap, nu alocarea unei noi memorii heap pentru a păstra o instanță a acelui tip de referință), deci nu ne așteptăm să apară gunoi aici. Enumeratorul în sine poate fi boxat în anumite circumstanțe și poate ajunge pe heap, dar aceste cazuri se aplică și în cazul tipurilor de date primitive.  > Por DMGregory.