Pourquoi la boucle for est-elle plus efficace que la boucle for pour chaque boucle dans Unity?


8

Lors d'un discours Unity sur les performances, le gars nous a dit d'éviter la for eachboucle pour augmenter les performances et d'utiliser à la place la boucle normale. Quelle est la différence entre foret for eachdu point de vue de la mise en œuvre? Qu'est-ce qui rend for eachinefficace?


1
En plus de compter le nombre d'éléments dans le tableau ou quoi que ce soit que vous utilisez, il ne devrait pas y avoir beaucoup de différence. Avec une recherche rapide sur Google, j'ai trouvé ceci: stackoverflow.com/questions/3430194/… (Cela s'appliquera également à Unity, même si la langue est différente, c'est toujours la même logique de base qui s'applique)
John Hamilton

2
J'ajouterais: dans votre première itération, n'affinez pas les performances, mais une priorité devrait être une architecture propre et un style de code. Optimisez (les performances) uniquement si vous en avez besoin plus tard ...
Marco

2
Avez-vous un lien vers cette conférence?
Vaillancourt

2
Selon cette page Unity , les for eachboucles itérant sur autre chose qu'un tableau génèrent des ordures (versions Unity antérieures à 5.5)
Hellium

2
Je vois un vote pour fermer cela comme hors sujet parce que c'est une programmation générale, mais comme c'est demandé dans le contexte d'Unity en particulier et que c'est un comportement qui a changé entre les versions d'Unity, je pense qu'il vaut la peine de rester ouvert ici pour référence de Développeurs de jeux Unity.
DMGregory

Réponses:


13

Une boucle foreach crée ostensiblement un objet IEnumerator à partir de la collection que vous lui avez passée, puis passe par-dessus. Donc, une boucle comme celle-ci:

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

se traduit par quelque chose comme ceci:
(éluder une certaine complexité autour de la façon dont l'énumérateur est éliminé plus tard)

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

Si cela est compilé naïvement / directement, cet objet énumérateur est alloué sur le tas (ce qui prend un peu de temps), même si son type sous-jacent est un struct - quelque chose appelé boxe. Une fois la boucle terminée, cette allocation devient des ordures à nettoyer plus tard, et votre jeu peut bégayer pendant un moment si suffisamment de déchets s'empilent pour que le collecteur exécute un balayage complet. Enfin, la MoveNextméthode et la Currentpropriété peuvent impliquer plus d'étapes d'appel de fonction que de simplement saisir un élément directement avec collection[i], si le compilateur n'inline pas cette action.

Les liens Unity docs Hellium ci - dessus indiquent que cette forme naïve est exactement ce qui se passe dans Unity avant la version 5.5 , si elle itère sur autre chose qu'un tableau natif.

Dans la version 5.6+ (y compris les versions 2017), cette boxe inutile a disparu et vous ne devriez pas rencontrer d'allocation inutile si vous utilisez un type concret (par exemple, int[]ou List<GameObject>ou Queue<T>).

Vous obtiendrez toujours une allocation si la méthode en question sait seulement qu'elle fonctionne avec une sorte d'IList ou une autre interface - dans ces cas, elle ne sait pas avec quel énumérateur spécifique elle travaille, elle doit donc la placer dans un objet IEnumerator en premier. .

Il y a donc de fortes chances que ce soit de vieux conseils qui ne sont pas si importants dans le développement moderne de Unity. L' unité devs comme nous pouvons être un peu superstitieux choses qui utilisé pour être poilu lent / dans les anciennes versions, afin de prendre des conseils de cette forme avec un grain de sel. ;) Le moteur évolue plus vite que nos croyances à ce sujet.

Dans la pratique, je n'ai jamais vu un jeu présenter une lenteur notable ou un hoquet de collecte de déchets à l'aide de boucles foreach, mais votre kilométrage peut varier. Profilez votre jeu sur le matériel que vous avez choisi pour voir si cela vaut la peine de s'inquiéter. Si vous en avez besoin, il est assez trivial de remplacer les boucles foreach par des boucles for explicites plus tard dans le développement si un problème se manifeste, donc je ne sacrifierais pas la lisibilité et la facilité de codage au début du développement.


Juste pour vérifier, un List<MyCustomType>et IEnumerator<MyCustomType> getListEnumerator() { }ça va, alors qu'une méthode IEnumerator getListEnumerator() { }ne le serait pas.
Draco18 ne font plus confiance au SE

0

Si un pour chaque boucle utilisé pour la collection ou un tableau d'objets (c'est-à-dire un tableau de tous les éléments autres que le type de données primitif), GC (Garbage Collector) est appelé pour libérer l'espace de la variable de référence à la fin d'un pour chaque boucle.

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

Alors que la boucle for est utilisée pour itérer sur les éléments à l'aide d'un index et donc le type de données primitif n'affectera pas les performances par rapport à un type de données non primitif.

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

Avez-vous une citation pour cela? La tempvariable ici peut être allouée sur la pile, même si la collection est pleine de types de référence (car c'est juste une référence aux entrées existantes déjà sur le tas, pas d'allocation de nouvelle mémoire de tas pour contenir une instance de ce type de référence), donc nous ne nous attendrions pas à ce que des ordures se produisent ici. L'énumérateur lui-même peut être encadré dans certaines circonstances et se retrouver sur le tas, mais ces cas s'appliquent également aux types de données primitifs.
DMGregory
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.