Parece-me que as pessoas não gostam muito de uma goto
declaração, então senti a necessidade de esclarecer um pouco isso.
Acredito que as 'emoções' que as pessoas goto
acabam se resumindo à compreensão do código e (equívocos) sobre possíveis implicações no desempenho. Antes de responder à pergunta, portanto, primeiro vou entrar em alguns detalhes de como ela é compilada.
Como todos sabemos, o C # é compilado para IL, que é compilado para o assembler usando um compilador SSA. Darei algumas idéias sobre como tudo isso funciona e, em seguida, tentarei responder à pergunta em si.
De C # para IL
Primeiro, precisamos de um pedaço de código C #. Vamos começar simples:
foreach (var item in array)
{
// ...
break;
// ...
}
Vou fazer isso passo a passo para lhe dar uma boa idéia do que acontece sob o capô.
Primeira tradução: de foreach
para o for
loop equivalente (Nota: estou usando uma matriz aqui, porque não quero entrar em detalhes de IDisposable - nesse caso, eu também teria que usar um IEnumerable):
for (int i=0; i<array.Length; ++i)
{
var item = array[i];
// ...
break;
// ...
}
Segunda tradução: o for
e break
é traduzido em um equivalente mais fácil:
int i=0;
while (i < array.Length)
{
var item = array[i];
// ...
break;
// ...
++i;
}
E terceira tradução (isso é equivalente ao código IL): mudamos break
e while
em uma agência:
int i=0; // for initialization
startLoop:
if (i >= array.Length) // for condition
{
goto exitLoop;
}
var item = array[i];
// ...
goto exitLoop; // break
// ...
++i; // for post-expression
goto startLoop;
Embora o compilador faça essas coisas em uma única etapa, ele fornece informações sobre o processo. O código IL que evolui do programa C # é o tradução literal do último código C #. Você pode ver por si mesmo aqui: https://dotnetfiddle.net/QaiLRz (clique em 'ver IL')
Agora, uma coisa que você observou aqui é que, durante o processo, o código se torna mais complexo. A maneira mais fácil de observar isso é pelo fato de precisarmos de mais e mais códigos para realizar a mesma coisa. Você também pode argumentar que foreach
,for
, while
e break
são realmente curtos mãos para goto
, que é parcialmente verdadeiro.
De IL para Assembler
O compilador .NET JIT é um compilador SSA. Não vou entrar em todos os detalhes do formulário SSA aqui e como criar um compilador otimizador, é demais, mas posso fornecer um entendimento básico sobre o que acontecerá. Para uma compreensão mais profunda, é melhor começar a ler sobre a otimização de compiladores (eu gosto deste livro para uma breve introdução: http://ssabook.gforge.inria.fr/latest/book.pdf ) e LLVM (llvm.org) .
Todo compilador de otimização depende do fato de que o código é fácil e segue padrões previsíveis . No caso dos loops FOR, usamos a teoria dos grafos para analisar ramificações e, em seguida, otimizamos coisas como cycli em nossas ramificações (por exemplo, ramificações para trás).
No entanto, agora temos ramificações avançadas para implementar nossos loops. Como você deve ter adivinhado, esta é realmente uma das primeiras etapas que o JIT corrigirá, assim:
int i=0; // for initialization
if (i >= array.Length) // for condition
{
goto endOfLoop;
}
startLoop:
var item = array[i];
// ...
goto endOfLoop; // break
// ...
++i; // for post-expression
if (i >= array.Length) // for condition
{
goto startLoop;
}
endOfLoop:
// ...
Como você pode ver, agora temos um ramo inverso, que é nosso pequeno loop. A única coisa que ainda é desagradável aqui é o ramo com o qual acabamos devido à nossa break
declaração. Em alguns casos, podemos mudar isso da mesma maneira, mas em outros, existe para ficar.
Então, por que o compilador faz isso? Bem, se conseguirmos desenrolar o loop, poderemos vetorizá-lo. Podemos até provar que há apenas constantes sendo adicionadas, o que significa que todo o nosso loop pode desaparecer no ar. Para resumir: tornando os padrões previsíveis (tornando os ramos previsíveis), podemos provar que certas condições se mantêm em nosso loop, o que significa que podemos fazer mágica durante a otimização do JIT.
No entanto, os ramos tendem a quebrar esses bons padrões previsíveis, o que é algo que otimiza, portanto, um desagrado. Quebre, continue, vá - todos eles pretendem quebrar esses padrões previsíveis - e, portanto, não são realmente "agradáveis".
Você também deve perceber, neste ponto, que um simples foreach
é mais previsível do que um monte degoto
declarações que se espalham por todo o lugar. Em termos de (1) legibilidade e (2) da perspectiva do otimizador, é a melhor solução.
Outra coisa que vale a pena mencionar é que é muito relevante para a otimização de compiladores atribuir registros a variáveis (um processo chamado alocação de registros ). Como você deve saber, existe apenas um número finito de registros em sua CPU e eles são de longe os pedaços de memória mais rápidos em seu hardware. As variáveis usadas no código que está no loop mais interno têm mais probabilidade de obter um registro atribuído, enquanto as variáveis fora do loop são menos importantes (porque esse código provavelmente é atingido menos).
Ajuda, muita complexidade ... o que devo fazer?
A conclusão é que você deve sempre usar as construções de linguagem que tem à sua disposição, que geralmente (implicitamente) criam padrões previsíveis para o seu compilador. Tente evitar ramos estranhos se possível (especificamente: break
, continue
, goto
ou return
no meio do nada).
A boa notícia aqui é que esses padrões previsíveis são fáceis de ler (para humanos) e fáceis de detectar (para compiladores).
Um desses padrões é chamado SESE, que significa Saída única de entrada única.
E agora chegamos à verdadeira questão.
Imagine que você tem algo parecido com isto:
// a is a variable.
for (int i=0; i<100; ++i)
{
for (int j=0; j<100; ++j)
{
// ...
if (i*j > a)
{
// break everything
}
}
}
A maneira mais fácil de fazer disso um padrão previsível é simplesmente eliminar if
completamente:
int i, j;
for (i=0; i<100 && i*j <= a; ++i)
{
for (j=0; j<100 && i*j <= a; ++j)
{
// ...
}
}
Em outros casos, você também pode dividir o método em 2 métodos:
// Outer loop in method 1:
for (i=0; i<100 && processInner(i); ++i)
{
}
private bool processInner(int i)
{
int j;
for (j=0; j<100 && i*j <= a; ++j)
{
// ...
}
return i*j<=a;
}
Variáveis temporárias? Bom, ruim ou feio?
Você pode até decidir retornar um booleano de dentro do loop (mas eu pessoalmente prefiro o formulário SESE, porque é assim que o compilador o verá e acho que é mais fácil de ler).
Algumas pessoas pensam que é mais limpo usar uma variável temporária e propõem uma solução como esta:
bool more = true;
for (int i=0; i<100; ++i)
{
for (int j=0; j<100; ++j)
{
// ...
if (i*j > a) { more = false; break; } // yuck.
// ...
}
if (!more) { break; } // yuck.
// ...
}
// ...
Pessoalmente, sou contra essa abordagem. Veja novamente como o código é compilado. Agora pense no que isso fará com esses padrões agradáveis e previsíveis. Obter a foto?
Certo, deixe-me explicar. O que acontecerá é que:
- O compilador escreverá tudo como ramificações.
- Como uma etapa de otimização, o compilador fará a análise do fluxo de dados na tentativa de remover a
more
variável estranha que só é usada no fluxo de controle.
- Se for bem-sucedida, a variável
more
será eliminada do programa e apenas as ramificações permanecerão. Essas ramificações serão otimizadas, para que você obtenha apenas uma ramificação fora do loop interno.
- Se malsucedida, a variável
more
é definitivamente usada no loop mais interno; portanto, se o compilador não a otimizar, há uma grande chance de ser alocado para um registro (que consome uma memória valiosa do registro).
Portanto, para resumir: o otimizador em seu compilador enfrentará muitos problemas para descobrir que more
é usado apenas para o fluxo de controle e, na melhor das hipóteses , o converterá em um único ramo fora do externo para ciclo.
Em outras palavras, o melhor cenário é que ele acabe com o equivalente a isso:
for (int i=0; i<100; ++i)
{
for (int j=0; j<100; ++j)
{
// ...
if (i*j > a) { goto exitLoop; } // perhaps add a comment
// ...
}
// ...
}
exitLoop:
// ...
Minha opinião pessoal sobre isso é bastante simples: se é isso que pretendemos o tempo todo, vamos facilitar o mundo para o compilador e a legibilidade, e escreva isso imediatamente.
tl; dr:
Bottom line:
- Use uma condição simples no seu loop for, se possível. Atenha-se ao máximo das construções de linguagem de alto nível que você tiver à sua disposição.
- Se tudo falhar e você ficar com um
goto
ou outro bool more
, prefira o primeiro.