¿Por qué el bucle for es más eficiente que para cada bucle en Unity?

8

Durante una charla de Unity sobre el rendimiento, el chico nos dijo que evitáramos el for eachbucle para aumentar el rendimiento y que usáramos el bucle normal. ¿Cuál es la diferencia entre fory for eachen el punto de vista de implementación? ¿Qué lo hace for eachineficiente?

Emad
fuente
1
Además de contar cuántos elementos hay en la matriz o lo que sea que use, no debería haber mucha diferencia. Con una búsqueda rápida en Google, encontré esto: stackoverflow.com/questions/3430194/… (Esto también se aplicará a Unity, aunque el idioma es diferente, sigue siendo la misma lógica base que se aplica)
John Hamilton
2
Añadiría: en su primera iteración, no ajuste el rendimiento, pero una prioridad debe ser una arquitectura limpia y un estilo de código. Solo optimice (rendimiento) si es necesario más tarde ...
Marco
2
¿Tienes un enlace a esa charla?
Vaillancourt
2
De acuerdo con esta página de Unity , los for eachbucles que iteran sobre cualquier cosa que no sea una matriz genera basura (versiones de Unity anteriores a 5.5)
Hellium
2
Veo un voto para cerrar esto como fuera de tema porque es una programación general, pero dado que se pregunta en el contexto de Unity en particular y este es un comportamiento que ha cambiado entre las versiones de Unity, creo que vale la pena mantenerlo abierto aquí para referencia de Desarrolladores de juegos de Unity.
DMGregory

Respuestas:

13

Un bucle foreach aparentemente crea un objeto IEnumerator de la colección que lo pasa y luego lo recorre. Entonces un bucle como este:

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

se traduce en algo parecido a esto:
(evitando cierta complejidad en torno a cómo se eliminará el enumerador más adelante)

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

Si esto se compila ingenua / directamente, entonces ese objeto enumerador se asigna en el montón (lo que lleva un poco de tiempo), incluso si su tipo subyacente es una estructura, algo llamado boxeo. Después de completar el ciclo, esta asignación se convierte en basura para limpiar más tarde, y su juego puede tartamudear por un momento si se acumula suficiente basura para que el recolector ejecute un barrido completo. Por último, el MoveNextmétodo y la Currentpropiedad podrían involucrar más pasos de llamada a funciones que simplemente tomar un elemento directamente collection[i], si el compilador no integra esa acción.

Los enlaces de Unity docs Hellium anteriores indican que esta forma ingenua es exactamente lo que sucede en Unity antes de la versión 5.5 , si se itera sobre otra cosa que no sea una matriz nativa.

En la versión 5.6+ (incluidas las versiones de 2017), este boxeo innecesario desapareció , y no debería experimentar una asignación innecesaria si está utilizando algún tipo concreto (por ejemplo, int[]or List<GameObject>o Queue<T>).

Todavía obtendrá una asignación si el método en cuestión solo sabe que está funcionando con algún tipo de IList u otra interfaz ; en esos casos, no sabe con qué enumerador específico está trabajando, por lo que primero debe encajonarlo en un objeto IEnumerator .

Entonces, hay una buena posibilidad de que este sea un viejo consejo que no es tan importante en el desarrollo moderno de Unity. Los desarrolladores de Unity como nosotros pueden ser un poco supersticiosos acerca de las cosas que solían ser lentas / peludas en las versiones antiguas, así que toma cualquier consejo de esa forma con un grano de sal. ;) El motor evoluciona más rápido que nuestras creencias al respecto.

En la práctica, nunca he tenido un juego que muestre una notable lentitud o problemas de recolección de basura al usar bucles foreach, pero su millaje puede variar. Perfile su juego en su hardware elegido para ver si vale la pena preocuparse. Si es necesario, es bastante trivial reemplazar los bucles foreach con bucles explícitos para más adelante en el desarrollo en caso de que se manifieste un problema, por lo que no sacrificaría la legibilidad y la facilidad de codificación al principio del desarrollo.

DMGregory
fuente
Sólo para comprobar, una List<MyCustomType>y IEnumerator<MyCustomType> getListEnumerator() { }están muy bien, mientras que un método IEnumerator getListEnumerator() { }no lo sería.
Draco18s ya no confía en SE
0

Si se utiliza un para cada bucle para la recopilación o matriz de objetos (es decir, la matriz de todos los elementos que no sean el tipo de datos primitivo), GC (Garbage Collector) se llama al espacio libre de la variable de referencia al final de a para cada bucle.

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

Mientras que for loop se usa para iterar sobre los elementos usando un índice y, por lo tanto, el tipo de datos primitivo no afectará el rendimiento en comparación con un tipo de datos no primitivo.

for (int i = 0; i < Collection.length; i++){
    //Get reference using index i;
    //Code Do Task
}
Tejas Jasani
fuente
¿Tienes una cita para esto? La tempvariable aquí se puede asignar en la pila, incluso si la colección está llena de tipos de referencia (ya que es solo una referencia a las entradas existentes que ya están en el montón, no se asigna nueva memoria de montón para contener una instancia de ese tipo de referencia), por lo que no esperaríamos que ocurra basura aquí. El enumerador en sí puede encuadrarse en algunas circunstancias y terminar en el montón, pero esos casos también se aplican a los tipos de datos primitivos.
DMGregory