Loop Foreach e inicialização variável

11

Existe uma diferença entre essas duas versões do código?

foreach (var thing in things)
{
    int i = thing.number;
    // code using 'i'
    // pay no attention to the uselessness of 'i'
}

int i;
foreach (var thing in things)
{
    i = thing.number;
    // code using 'i'
}

Ou o compilador não se importa? Quando falo da diferença, quero dizer em termos de desempenho e uso de memória. ..Ou basicamente qualquer diferença ou os dois acabam sendo o mesmo código após a compilação?

Alternatex
fonte
6
Você já tentou compilar os dois e ver a saída do bytecode?
4
@ MichaelT Eu não me sinto qualificado para comparar a saída de bytecode. Se eu encontrar uma diferença, não tenho certeza se seria capaz de entender exatamente o que isso significa.
Alternatex
4
Se for o mesmo, você não precisa ser qualificado.
1
@MichaelT Embora você precise ser qualificado o suficiente para adivinhar se o compilador poderia tê-lo otimizado e, em caso afirmativo, sob quais condições ele pode fazer essa otimização.
Ben Aaronson
@BenAaronson e isso provavelmente requer um exemplo não trivial para agradar essa funcionalidade.

Respostas:

22

TL; DR - são exemplos equivalentes na camada IL.


O DotNetFiddle torna isso bonito de responder, pois permite ver a IL resultante.

Usei uma variação ligeiramente diferente da sua construção de loop para tornar meus testes mais rápidos. Eu usei:

Variação 1:

using System;

public class Program
{
    public static void Main()
    {
        Console.WriteLine("Hello World");
        int x;
        int i;

        for(x=0; x<=2; x++)
        {
            i = x;
            Console.WriteLine(i);
        }
    }
}

Variação 2:

        Console.WriteLine("Hello World");
        int x;

        for(x=0; x<=2; x++)
        {
            int i = x;
            Console.WriteLine(i);
        }

Nos dois casos, a saída IL compilada foi renderizada da mesma forma.

.class public auto ansi beforefieldinit Program
       extends [mscorlib]System.Object
{
  .method public hidebysig static void  Main() cil managed
  {
    // 
    .maxstack  2
    .locals init (int32 V_0,
             int32 V_1,
             bool V_2)
    IL_0000:  nop
    IL_0001:  ldstr      "Hello World"
    IL_0006:  call       void [mscorlib]System.Console::WriteLine(string)
    IL_000b:  nop
    IL_000c:  ldc.i4.0
    IL_000d:  stloc.0
    IL_000e:  br.s       IL_001f

    IL_0010:  nop
    IL_0011:  ldloc.0
    IL_0012:  stloc.1
    IL_0013:  ldloc.1
    IL_0014:  call       void [mscorlib]System.Console::WriteLine(int32)
    IL_0019:  nop
    IL_001a:  nop
    IL_001b:  ldloc.0
    IL_001c:  ldc.i4.1
    IL_001d:  add
    IL_001e:  stloc.0
    IL_001f:  ldloc.0
    IL_0020:  ldc.i4.2
    IL_0021:  cgt
    IL_0023:  ldc.i4.0
    IL_0024:  ceq
    IL_0026:  stloc.2
    IL_0027:  ldloc.2
    IL_0028:  brtrue.s   IL_0010

    IL_002a:  ret
  } // end of method Program::Main

Então, para responder à sua pergunta: o compilador otimiza a declaração da variável e torna as duas variações equivalentes.

Para meu entendimento, o compilador .NET IL move todas as declarações de variáveis ​​para o início da função, mas não consegui encontrar uma boa fonte que indicasse claramente 2 . Neste exemplo em particular, você vê que os moveu com esta declaração:

    .locals init (int32 V_0,
             int32 V_1,
             bool V_2)

Onde ficamos um pouco obsessivos em fazer comparações ...

Caso A, todas as variáveis ​​são movidas para cima?

Para aprofundar um pouco mais, testei a seguinte função:

public static void Main()
{
    Console.WriteLine("Hello World");
    int x=5;

    if (x % 2==0) 
    { 
        int i = x; 
        Console.WriteLine(i); 
    }
    else 
    { 
        string j = x.ToString(); 
        Console.WriteLine(j); 
    } 
}

A diferença aqui é que declaramos um int iou um string jcom base na comparação. Novamente, o compilador move todas as variáveis ​​locais para o topo da função 2 com:

.locals init (int32 V_0,
         int32 V_1,
         string V_2,
         bool V_3)

Achei interessante notar que, embora int inão seja declarado neste exemplo, o código para suportá-lo ainda é gerado.

Caso B: E em foreachvez de for?

Foi apontado que ele foreachtem um comportamento diferente fore que eu não estava verificando a mesma coisa que havia sido perguntada. Então, eu coloquei nessas duas seções de código para comparar a IL resultante.

int declaração fora do loop:

    Console.WriteLine("Hello World");
    List<int> things = new List<int>(){1, 2, 3, 4, 5};
    int i;

    foreach(var thing in things)
    {
        i = thing;
        Console.WriteLine(i);
    }

int declaração dentro do loop:

    Console.WriteLine("Hello World");
    List<int> things = new List<int>(){1, 2, 3, 4, 5};

    foreach(var thing in things)
    {
        int i = thing;
        Console.WriteLine(i);
    }

A IL resultante com o foreachloop era de fato diferente da IL gerada usando o forloop. Especificamente, o bloco init e a seção do loop foram alterados.

.locals init (class [mscorlib]System.Collections.Generic.List`1<int32> V_0,
         int32 V_1,
         int32 V_2,
         class [mscorlib]System.Collections.Generic.List`1<int32> V_3,
         valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32> V_4,
         bool V_5)
...
.try
{
  IL_0045:  br.s       IL_005a

  IL_0047:  ldloca.s   V_4
  IL_0049:  call       instance !0 valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>::get_Current()
  IL_004e:  stloc.1
  IL_004f:  nop
  IL_0050:  ldloc.1
  IL_0051:  stloc.2
  IL_0052:  ldloc.2
  IL_0053:  call       void [mscorlib]System.Console::WriteLine(int32)
  IL_0058:  nop
  IL_0059:  nop
  IL_005a:  ldloca.s   V_4
  IL_005c:  call       instance bool valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>::MoveNext()
  IL_0061:  stloc.s    V_5
  IL_0063:  ldloc.s    V_5
  IL_0065:  brtrue.s   IL_0047

  IL_0067:  leave.s    IL_0078

}  // end .try
finally
{
  IL_0069:  ldloca.s   V_4
  IL_006b:  constrained. valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>
  IL_0071:  callvirt   instance void [mscorlib]System.IDisposable::Dispose()
  IL_0076:  nop
  IL_0077:  endfinally
}  // end handler

A foreachabordagem gerou mais variáveis ​​locais e exigiu algumas ramificações adicionais. Essencialmente, na primeira vez, ele pula para o final do loop para obter a primeira iteração da enumeração e depois volta para quase a parte superior do loop para executar o código do loop. Em seguida, continua a percorrer conforme o esperado.

Porém, além das diferenças de ramificação causadas pelo uso das construções fore foreach, não houve diferença na IL com base no local em que a int ideclaração foi colocada. Portanto, ainda estamos nas duas abordagens sendo equivalentes.

Caso C: E as diferentes versões do compilador?

Em um comentário deixado 1 , havia um link para uma pergunta de SO referente a um aviso sobre acesso variável ao foreach e uso de fechamento . A parte que realmente chamou minha atenção nessa pergunta foi que pode ter havido diferenças em como o compilador .NET 4.5 funcionava em relação às versões anteriores do compilador.

E foi aí que o site DotNetFiddler me decepcionou - tudo o que eles tinham disponível era o .NET 4.5 e uma versão do compilador Roslyn. Então, criei uma instância local do Visual Studio e comecei a testar o código. Para garantir que eu comparasse as mesmas coisas, comparei o código criado localmente no .NET 4.5 com o código DotNetFiddler.

A única diferença que notei foi com o bloco init local e a declaração da variável. O compilador local foi um pouco mais específico ao nomear as variáveis.

  .locals init ([0] class [mscorlib]System.Collections.Generic.List`1<int32> things,
           [1] int32 thing,
           [2] int32 i,
           [3] class [mscorlib]System.Collections.Generic.List`1<int32> '<>g__initLocal0',
           [4] valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32> CS$5$0000,
           [5] bool CS$4$0001)

Mas com essa pequena diferença, foi tão longe, tão bom. Eu tinha saída IL equivalente entre o compilador DotNetFiddler e o que minha instância local do VS estava produzindo.

Então, reconstruí o projeto direcionado ao .NET 4, .NET 3.5 e, em boa medida, ao modo .NET 3.5 Release.

E nos três casos adicionais, a IL gerada era equivalente. A versão do .NET direcionada não teve efeito na IL gerada nessas amostras.


Para resumir esta aventura: Acho que podemos dizer com segurança que o compilador não se importa com o local onde você declara o tipo primitivo e que não há efeito na memória ou no desempenho com qualquer método de declaração. E isso é válido independentemente do uso de um forou foreachloop.

Eu considerei executar outro caso que incorporava um fechamento dentro do foreachloop. Mas você perguntou sobre os efeitos de onde uma variável do tipo primitiva foi declarada, então imaginei que estava indo muito além do que você estava interessado em perguntar. A pergunta SO mencionada anteriormente tem uma ótima resposta que fornece uma boa visão geral sobre os efeitos de fechamento nas variáveis ​​de iteração foreach.

1 Obrigado a Andy por fornecer o link original para a pergunta SO, abordando fechamentos em foreachloops.

2 Vale ressaltar que a especificação do ECMA-335 trata disso na seção I.12.3.2.2 'Variáveis ​​e argumentos locais'. Eu tive que ver a IL resultante e depois ler a seção para ficar claro sobre o que estava acontecendo. Obrigado à catraca por apontar isso no chat.

Comunidade
fonte
1
Pois e foreach não se comportam da mesma forma, e a pergunta inclui código diferente, que se torna importante quando há um fechamento no loop. stackoverflow.com/questions/14907987/…
Andy
1
@ Andy - obrigado pelo link! Fui em frente e verifiquei a saída gerada usando um foreachloop e também verifiquei a versão .NET direcionada.
0

Dependendo do compilador que você usa (nem sei se o C # tem mais de um), seu código será otimizado antes de ser transformado em um programa. Um bom compilador verá que você está reinicializando a mesma variável a cada vez com um valor diferente e gerenciando o espaço de memória para ela com eficiência.

Se você estivesse inicializando a mesma variável em uma constante cada vez, o compilador também a inicializaria antes do loop e a referenciaria.

Tudo depende de quão bem o seu compilador é escrito, mas no que diz respeito aos padrões de codificação, as variáveis ​​devem sempre ter o menor escopo possível . Então, declarar dentro do loop é o que sempre fui ensinado.

leylandski
fonte
3
Se o seu último parágrafo é verdadeiro ou não, isso depende de duas coisas: a importância de minimizar o escopo da variável no contexto exclusivo do seu próprio programa e o conhecimento interno do compilador sobre se ele realmente otimiza ou não as várias atribuições.
Robert Harvey
E há o tempo de execução, que traduz ainda mais o código de bytes em linguagem de máquina, onde muitas dessas mesmas otimizações (discutidas aqui como otimizações de compilador) também são executadas.
Erik Eidt
-2

em primeiro lugar, você está apenas declarando e inicializando o loop interno para que, sempre que o loop for repetido, ele será reinicializado "i" no loop interno. No segundo, você está declarando apenas fora do loop.

user304046
fonte
1
este não parece oferecer nada substancial sobre pontos feitos e explicado em cima resposta que foi publicado mais de 2 anos atrás
mosquito
2
Obrigado por dar uma resposta, mas ele não fornece nenhum aspecto novo que a resposta aceita e com melhor classificação ainda não cobre (em detalhes).
CharonX