Por que o loop for é mais eficiente do que o loop de cada loop no Unity?

8

Durante uma palestra do Unity sobre desempenho, o cara nos disse para evitar o for eachloop para aumentar o desempenho e usar o loop for normal. Qual é a diferença entre fore for eachno ponto de vista da implementação? O que torna for eachineficiente?

Emad
fonte
11
Além de contar quantos elementos existem na matriz ou o que você usar, não deve haver muita diferença. Com uma rápida pesquisa no google, encontrei o seguinte: stackoverflow.com/questions/3430194/… (Isso também se aplica ao Unity, mesmo que o idioma seja diferente, ainda é a mesma lógica básica que se aplica)
John Hamilton,
2
Eu acrescentaria: Na sua primeira iteração, não ajuste o desempenho, mas uma prioridade deve ser uma arquitetura e um estilo de código limpos. Apenas optimize (performance) se houver necessidade disso mais tarde ...
Marco
2
Você tem um link para essa conversa?
Vaillancourt
2
De acordo com esta página do Unity , os for eachloops que iteram sobre qualquer coisa que não seja uma matriz geram lixo (versões do Unity anteriores à 5.5) #
Hellium 25/17/17
2
Vejo uma votação para encerrar isso como fora de tópico, porque é uma programação geral, mas, como é solicitado no contexto do Unity em particular, e este é um comportamento que mudou entre as versões do Unity, acho que vale a pena manter aberto aqui para referência de Desenvolvedores de jogos Unity.
DMGregory

Respostas:

13

Um loop foreach ostensivamente cria um objeto IEnumerator a partir da coleção que você passa e, em seguida, passa por ele. Então, um loop como este:

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

traduz para algo mais ou menos assim:
(elimina alguma complexidade sobre como o enumerador é descartado posteriormente)

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

Se isso for compilado de forma ingênua / direta, esse objeto enumerador será alocado no heap (o que leva um pouco de tempo), mesmo que seu tipo subjacente seja uma estrutura - algo chamado boxe. Após a conclusão do loop, essa alocação se torna lixo para limpeza posterior, e seu jogo pode gaguejar por um momento, se houver lixo suficiente para o coletor executar uma varredura completa. Por fim, o MoveNextmétodo e a Currentpropriedade podem envolver mais etapas de chamada de função do que simplesmente pegar um item diretamente collection[i], se o compilador não incorporar essa ação.

Os links Hellium dos documentos do Unity acima indicam que essa forma ingênua é exatamente o que acontece no Unity antes da versão 5.5 , se iterar sobre algo diferente de uma matriz nativa.

Na versão 5.6+ (incluindo as versões de 2017), esse boxe desnecessário desapareceu e você não deve ter alocação desnecessária se estiver usando algum tipo concreto (por exemplo, int[]ou List<GameObject>ou Queue<T>).

Você ainda receberá uma alocação se o método em questão souber apenas que está trabalhando com algum tipo de IList ou outra interface - nesses casos, ele não sabe com qual enumerador específico está trabalhando, portanto, deve colocá-lo em um objeto IEnumerator primeiro .

Portanto, há uma boa chance de que este seja um conselho antigo que não é tão importante no desenvolvimento moderno do Unity. Os desenvolvedores de unidade como nós podem ser um pouco supersticiosos em relação às coisas que costumavam ser lentas / peludas em versões antigas; portanto, aceite qualquer conselho dessa forma com um pouco de sal. ;) O mecanismo evolui mais rapidamente do que nossas crenças sobre ele.

Na prática, eu nunca tive um jogo exibindo lentidão perceptível ou soluços de coleta de lixo usando loops de foreach, mas sua milhagem pode variar. Faça o perfil do seu jogo no hardware escolhido para ver se vale a pena se preocupar. Se você precisar, é bastante trivial substituir loops foreach por loops explícitos mais tarde no desenvolvimento, caso um problema se manifeste, para que eu não sacrifique a legibilidade e a facilidade de codificação no início do desenvolvimento.

DMGregory
fonte
Apenas para verificar, a List<MyCustomType>e IEnumerator<MyCustomType> getListEnumerator() { }estão bem, enquanto um método IEnumerator getListEnumerator() { }não seria.
Draco18s não confia mais em SE
0

Se a para cada loop usado para coleta ou matriz de objeto (ou seja, matriz de todos os elementos, exceto o tipo de dados primitivo), o GC (Garbage Collector) é chamado para liberar espaço da variável de referência no final de uma para cada loop.

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

Enquanto o loop for é usado para iterar sobre os elementos usando um índice e, portanto, o tipo de dados primitivo não afetará o desempenho em comparação com um tipo de dados não primitivo.

for (int i = 0; i < Collection.length; i++){
    //Get reference using index i;
    //Code Do Task
}
Tejas Jasani
fonte
Você tem uma citação para isso? A tempvariável aqui pode ser alocada na pilha, mesmo que a coleção esteja cheia de tipos de referência (já que é apenas uma referência às entradas existentes já no heap, não alocando nova memória heap para armazenar uma instância desse tipo de referência), portanto não esperamos que o lixo ocorra aqui. O próprio enumerador pode ser encaixotado em algumas circunstâncias e acabar no heap, mas esses casos também se aplicam a tipos de dados primitivos.
DMGregory