Цикл Foreach и инициализация переменной


11

Есть ли разница между этими двумя версиями кода?

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'
}

Или компилятору все равно? Когда я говорю о разнице, я имею в виду производительность и использование памяти. ... Или, в принципе, какая-то разница или эти два кода после компиляции оказываются одним и тем же?


6
Вы пытались скомпилировать два и посмотреть вывод байт-кода?

4
@MichaelT Я не чувствую, что у меня есть право сравнивать выходные данные байт-кода. Если я найду разницу, я не уверен, что смогу понять, что именно это означает.
Alternatex

4
Если это то же самое, вам не нужно быть квалифицированным.

1
@MichaelT Хотя вам нужно быть достаточно квалифицированным, чтобы правильно догадаться, мог ли компилятор оптимизировать его, и если да, то при каких условиях он может выполнить эту оптимизацию.
Бен Ааронсон

@BenAaronson и это, вероятно, требует нетривиального примера, чтобы щекотать эту функциональность.

Ответы:


22

TL; DR - это эквивалентные примеры на уровне IL.


DotNetFiddle делает этот ответ довольно симпатичным, так как он позволяет увидеть итоговый IL.

Я использовал немного другой вариант вашей конструкции цикла, чтобы ускорить тестирование. Я использовал:

Вариация 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);
        }
    }
}

Вариация 2:

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

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

В обоих случаях скомпилированный вывод IL отображался одинаково.

.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

Итак, чтобы ответить на ваш вопрос: компилятор оптимизирует объявление переменной и делает два варианта эквивалентными.

Насколько я понимаю, компилятор .NET IL перемещает все объявления переменных в начало функции, но я не смог найти хороший источник, который четко заявил, что 2 . В этом конкретном примере вы видите, что он переместил их с этим утверждением:

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

При этом мы становимся слишком одержимыми в сравнении ...

Случай А, все переменные перемещены вверх?

Чтобы углубиться в это, я протестировал следующую функцию:

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); 
    } 
}

Разница здесь в том , что мы объявляем либо int iили на string jоснове сравнения. Опять же, компилятор перемещает все локальные переменные в верхнюю часть функции 2 с помощью:

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

Мне показалось интересным отметить, что, хотя int iв этом примере и не будет объявлено, код для его поддержки все еще генерируется.

Случай B: А что foreachвместо for?

Было отмечено, что foreachповедение отличается от того, forи я не проверял то, о чем спрашивали. Поэтому я вставил эти два раздела кода, чтобы сравнить полученный IL.

int объявление вне цикла:

    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 объявление внутри цикла:

    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);
    }

Результирующий IL с foreachпетлей действительно отличался от IL, сгенерированного с помощью forпетли. В частности, блок инициализации и секция цикла изменились.

.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

foreachПодход генерируется более локальных переменных , и требуется некоторое дополнительное разветвление. По сути, в первый раз он переходит к концу цикла, чтобы получить первую итерацию перечисления, а затем возвращается почти к вершине цикла, чтобы выполнить код цикла. Затем он продолжает проходить, как и следовало ожидать.

Но за ветвящееся различие , вызванным использования forи foreachконструкции, не была не разница в IL на основании которой int iдекларация была сделана. Таким образом, мы все еще находимся в двух подходах, являющихся эквивалентными.

Случай C: А как насчет разных версий компилятора?

В комментарии, который был оставлен 1 , была ссылка на вопрос SO, касающийся предупреждения о доступе к переменной с помощью foreach и использования замыкания . Часть, которая действительно привлекла мое внимание в этом вопросе, заключалась в том, что, возможно, были различия в работе компилятора .NET 4.5 по сравнению с более ранними версиями компилятора.

И вот где сайт DotNetFiddler подвел меня - все, что у них было доступно, - это .NET 4.5 и версия компилятора Roslyn. Поэтому я создал локальный экземпляр Visual Studio и начал тестировать код. Чтобы убедиться, что я сравнивал одни и те же вещи, я сравнил локально созданный код в .NET 4.5 с кодом DotNetFiddler.

Единственное отличие, которое я заметил, было в локальном блоке инициализации и объявлении переменной. Локальный компилятор был немного более конкретен в именовании переменных.

  .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)

Но с этой незначительной разницей это было так хорошо, так хорошо. У меня был эквивалентный вывод IL между компилятором DotNetFiddler и тем, что производил мой локальный экземпляр VS.

Поэтому я перестроил проект, нацеленный на .NET 4, .NET 3.5, и, для хорошей оценки, на режим .NET 3.5 Release.

И во всех трех из этих дополнительных случаев сгенерированный IL был эквивалентен. Целевая версия .NET не влияла на IL, сгенерированный в этих примерах.


Подводя итог этому приключению: я думаю, что мы можем с уверенностью сказать, что компилятору не важно, где вы объявляете примитивный тип, и что это не влияет на память или производительность ни одним из методов объявления. И это справедливо независимо от использования цикла forили foreach.

Я рассмотрел запуск еще одного случая, который включал замыкание внутри foreachцикла. Но вы спрашивали о влиянии того, где была объявлена ​​переменная примитивного типа, поэтому я подумал, что я слишком далеко зашел над тем, о чем вы хотели спросить. На вопрос SO, который я упоминал ранее, есть отличный ответ, который дает хороший обзор эффектов замыкания на итерационные переменные foreach.

1 Спасибо Энди за предоставленную оригинальную ссылку на вопрос SO, посвященный замыканиям внутри foreachциклов.

2 Стоит отметить, что в спецификации ECMA-335 это рассматривается в разделе I.12.3.2.2 «Локальные переменные и аргументы». Я должен был увидеть получившийся IL, а затем прочитать раздел, чтобы понять, что происходит. Спасибо фанатам за то, что указал на это в чате.


1
Ибо и foreach не ведут себя одинаково, и вопрос включает в себя другой код, который становится важным, когда в цикле есть замыкание. stackoverflow.com/questions/14907987/…
Энди

1
@ Энди - спасибо за ссылку! Я проверил сгенерированный вывод, используя foreachцикл, а также проверил целевую версию .NET.

0

В зависимости от того, какой компилятор вы используете (я даже не знаю, имеет ли C # более одного), ваш код будет оптимизирован перед тем, как превратиться в программу. Хороший компилятор увидит, что вы каждый раз переинициализируете одну и ту же переменную с другим значением и эффективно управляете пространством памяти.

Если бы вы каждый раз инициализировали одну и ту же переменную постоянной, компилятор также инициализировал бы ее перед циклом и ссылался на нее.

Все зависит от того, насколько хорошо написан ваш компилятор, но для стандартов кодирования переменные всегда должны иметь как можно меньшую область видимости. Так что объявление внутри цикла - это то, чему меня всегда учили.


3
Верен ли ваш последний абзац или нет, зависит от двух вещей: важности минимизации области видимости переменной в уникальном контексте вашей собственной программы и знания компилятора о том, действительно ли он оптимизирует несколько назначений.
Роберт Харви

Кроме того, существует среда выполнения, которая дополнительно переводит байт-код на машинный язык, где также выполняется множество таких же оптимизаций (обсуждаемых здесь как оптимизации компилятора).
Эрик Эйдт

-2

во-первых, вы просто объявляете и инициализируете внутри цикла, поэтому каждый цикл времени повторяет инициализацию внутри цикла. Во-вторых, вы только заявляете вне цикла.


1
Похоже, это не дает ничего существенного по поводу высказанных соображений и объяснений в верхнем ответе, который был опубликован более 2 лет назад
gnat

2
Спасибо, что дали ответ, но он не дает каких-либо новых аспектов, которые принятый, получивший самый высокий рейтинг ответ, еще не охватывает (подробно).
CharonX
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.