Програмування циклу та ініціалізація змінних


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 Я не відчуваю, що я кваліфікований для порівняння виводу байт-коду .. Якщо я знайду різницю, я не впевнений, що зможу зрозуміти, що це означає саме.
Альтернатекс

4
Якщо це те саме, вам не потрібно кваліфікуватися.

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

@BenAaronson, і це, ймовірно, вимагає нетривіального прикладу, щоб поставити ці функції.

Відповіді:


22

TL; DR - вони еквівалентні приклади на рівні IL.


DotNetFiddle змушує це відповісти, оскільки дозволяє побачити отриманий ІЛ.

Я скористався дещо іншим варіантом конструкції вашого циклу, щоб зробити тестування швидшим. Я використав:

Варіант 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те, що я не перевіряв те саме, про що питали. Тому я вкладаю в ці два розділи коду для порівняння отриманого ІЛ.

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

Отриманий ІЛ з foreachциклом справді відрізнявся від ІЛ, сформованого за допомогою forциклу. Зокрема, змінився блок init та розділ циклу.

.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 , знайшлося посилання на запитання про те, що стосується попередження про змінний доступ із функцією foreach та використання закриття . Частина, яка насправді зачепила мене в цьому питанні, полягала в тому, що, можливо, існували відмінності в тому, як компілятор .NET 4.5 працював проти попередніх версій компілятора.

І саме тут сайт DotNetFiddler мене підвів - все, що вони мали, -. NET 4.5 та версія компілятора Roslyn. Тому я вивів локальний екземпляр Visual Studio і почав тестувати код. Щоб переконатися, що я порівнював ті самі речі, я порівняв локально вбудований код у .NET 4.5 з кодом DotNetFiddler.

Єдина відмінність, яку я зазначив, була в локальному блоці init та оголошенні змінної. Локальний компілятор був дещо конкретнішим у назві змінних.

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

І в усіх трьох цих додаткових випадках генерований ІЛ був рівноцінним. Цільова версія .NET не впливала на ІЛ, що генерується в цих зразках.


Підсумовуючи цю пригоду: я думаю, що ми можемо з упевненістю сказати, що компілятору не важливо, де ви оголосили примітивний тип, і що жодним із способів декларування не впливає на пам'ять чи продуктивність. І це справедливо незалежно від використання forабо foreachциклу.

Я розглядав запуск ще одного випадку, який включав закриття всередині foreachциклу. Але ви запитували про наслідки декларування примітивної змінної типу, тож я зрозумів, що я заглиблююся далеко за рамки того, про що ви зацікавлені. Питання ТА, про яке я згадував раніше, має чудову відповідь, яка забезпечує хороший огляд ефектів закриття на змінні ітерації випередження.

1 Дякую Енді, що ви надали оригінальне посилання на питання SO, яке стосується закриттів в foreachциклі.

2 Варто зазначити, що специфікація ECMA-335 вирішує це у розділі I.12.3.2.2 "Локальні змінні та аргументи". Мені довелося побачити отриманий ІЛ, а потім прочитати розділ, щоб зрозуміти, що відбувається. Завдяки щурячому виродку за вказівку на це в чаті.


1
Тому що і foreach не поводяться однаково, і питання включає код, який є іншим, який стає важливим, коли в циклі є закриття. stackoverflow.com/questions/14907987/…
Енді

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

0

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

Якщо ви ініціалізували одну і ту ж змінну до постійної щоразу, компілятор також би ініціалізував її перед циклом і посилався на неї.

Все залежить від того, наскільки добре написаний ваш компілятор, але що стосується стандартів кодування, змінні завжди повинні мати найменшу можливу область. Тому декларувати всередині циклу - це те, чого я завжди вчив.


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

І тоді є час виконання, який додатково переводить байт-код на машинну мову, де також виконується багато таких самих оптимізацій (обговорюваних тут як оптимізація компілятора).
Ерік Ейдт

-2

спочатку ви просто оголошуєте та ініціалізуєте всередині циклу, тому кожен раз, коли цикл циклу, він буде повторно реалізований "я" всередині циклу. По-друге, ви оголошуєте лише зовнішню петлю.


1
це, здається, не пропонує нічого суттєвого щодо зафіксованих пунктів та пояснень у верхній відповіді, опублікованій понад 2 роки тому
gnat

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