Warum ist die for-Schleife effizienter als die für jede Schleife in Unity?

8

Während eines Unity-Gesprächs über die Leistung sagte uns der Typ, wir sollten eine for eachSchleife vermeiden , um die Leistung zu steigern, und stattdessen die normale for-Schleife verwenden. Was ist der Unterschied zwischen forund for eachin Bezug auf die Implementierung? Was macht for eachineffizient?

Emad
quelle
1
Abgesehen davon, wie viele Elemente sich im Array befinden oder was auch immer Sie verwenden, sollte es keinen großen Unterschied geben. Bei einer schnellen Google-Suche habe ich Folgendes gefunden: stackoverflow.com/questions/3430194/… (Dies gilt auch für Unity, obwohl die Sprache unterschiedlich ist, gilt immer noch dieselbe Basislogik )
John Hamilton
2
Ich würde hinzufügen: In Ihrer ersten Iteration sollten Sie die Leistung nicht optimieren, aber eine Priorität sollte eine saubere Architektur und ein sauberer Codestil sein. Nur optimieren (Leistung), wenn es später nötig ist ...
Marco
2
Hast du einen Link zu diesem Vortrag?
Vaillancourt
2
Gemäß dieser Seite der Einheit , for eachSchleifen etwas anderes Iterieren über als ein Array erzeugt Müll (Unity Versionen vor 5.5)
Hellium
2
Ich sehe eine Abstimmung zum Schließen als nicht zum Thema gehörend an, da es sich um eine allgemeine Programmierung handelt. Da dies jedoch insbesondere im Zusammenhang mit Unity gefragt wird und sich dieses Verhalten zwischen den Versionen von Unity geändert hat, ist es meines Erachtens wert, hier als Referenz offen zu bleiben Entwickler von Unity-Spielen.
DMGregory

Antworten:

13

Eine foreach-Schleife erstellt angeblich ein IEnumerator-Objekt aus der Sammlung, die Sie übergeben, und geht dann darüber hinweg. Also eine Schleife wie diese:

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

übersetzt sich in etwas wie
folgt : (einige Komplexität darüber, wie der Enumerator später entsorgt wird, entfällt)

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

Wenn dies naiv / direkt kompiliert wird, wird dieses Enumerator-Objekt auf dem Heap zugewiesen (was etwas Zeit in Anspruch nimmt), selbst wenn sein zugrunde liegender Typ eine Struktur ist - etwas, das als Boxen bezeichnet wird. Nachdem die Schleife abgeschlossen ist, wird diese Zuordnung zu Müll, um sie später zu bereinigen, und Ihr Spiel kann für einen Moment stottern, wenn sich genug Müll ansammelt, damit der Sammler einen vollständigen Sweep ausführen kann. Schließlich können die MoveNextMethode und die CurrentEigenschaft mehr Funktionsaufrufschritte umfassen, als nur ein Element direkt mit abzurufen collection[i], wenn der Compiler diese Aktion nicht einbindet.

Die Hellium-Links zu Unity-Dokumenten oben zeigen an, dass diese naive Form genau das ist, was in Unity vor Version 5.5 passiert , wenn über etwas anderes als ein natives Array iteriert wird.

In Version 5.6+ (einschließlich 2017-Versionen) ist dieses unnötige Boxen weg , und Sie sollten keine unnötige Zuordnung erfahren, wenn Sie einen konkreten Typ verwenden (z. B. int[]oder List<GameObject>oder Queue<T>).

Sie erhalten weiterhin eine Zuordnung, wenn die betreffende Methode nur weiß, dass sie mit einer Art IList oder einer anderen Schnittstelle funktioniert. In diesen Fällen weiß sie nicht, mit welchem ​​bestimmten Enumerator sie arbeitet, sodass sie zuerst in ein IEnumerator-Objekt gepackt werden muss .

Es besteht also eine gute Chance, dass dies ein alter Rat ist, der in der modernen Unity-Entwicklung nicht so wichtig ist. Unity-Entwickler wie wir können ein bisschen abergläubisch gegenüber Dingen sein, die in alten Versionen langsam / haarig waren. Nehmen Sie also jeden Rat dieser Form mit einem Körnchen Salz. ;) Der Motor entwickelt sich schneller als wir glauben.

In der Praxis hatte ich noch nie ein Spiel, das durch die Verwendung von foreach-Schleifen merkliche Langsamkeit oder Probleme mit der Speicherbereinigung aufwies, aber Ihr Kilometerstand kann variieren. Profilieren Sie Ihr Spiel auf der von Ihnen gewählten Hardware, um festzustellen, ob es sich überhaupt lohnt, sich Sorgen zu machen. Wenn Sie müssen, ist es ziemlich trivial, foreach-Schleifen später in der Entwicklung durch explizite for-Schleifen zu ersetzen, falls sich ein Problem manifestiert, damit ich nicht zu Beginn der Entwicklung auf Lesbarkeit und einfache Codierung verzichten würde.

DMGregory
quelle
Nur um zu überprüfen, a List<MyCustomType>und IEnumerator<MyCustomType> getListEnumerator() { }sind in Ordnung, während eine Methode IEnumerator getListEnumerator() { }nicht wäre.
Draco18s vertraut SE
0

Wenn a für jede Schleife, die für die Sammlung oder das Array eines Objekts verwendet wird (dh ein Array aller Elemente außer dem primitiven Datentyp), wird GC (Garbage Collector) aufgerufen, um am Ende von a für jede Schleife Speicherplatz der Referenzvariablen freizugeben.

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

Während die for-Schleife verwendet wird, um mithilfe eines Index über die Elemente zu iterieren, wirkt sich der primitive Datentyp im Vergleich zu einem nicht primitiven Datentyp nicht auf die Leistung aus.

for (int i = 0; i < Collection.length; i++){
    //Get reference using index i;
    //Code Do Task
}
Tejas Jasani
quelle
Hast du ein Zitat dafür? Die tempVariable hier kann auf dem Stapel zugewiesen werden, auch wenn die Auflistung voller Referenztypen ist (da es sich nur um eine Referenz auf die bereits auf dem Heap vorhandenen Einträge handelt und kein neuer Heapspeicher für eine Instanz dieses Referenztyps zugewiesen wird) Wir würden nicht erwarten, dass hier Müll anfällt. Der Enumerator selbst kann unter bestimmten Umständen eingerahmt sein und auf dem Heap landen. Diese Fälle gelten jedoch auch für primitive Datentypen.
DMGregory