Другой IL генерируется при добавлении еще одной переменной int

У меня есть эта программа на С#:

using System;

class Program
{
    public static void Main()
    {
    int i = 4;
    double d = 12.34;
    double PI = Math.PI;
    string name = "Ehsan";


    }
}

и когда я его скомпилирую, ниже приводится IL, сгенерированный компилятором для Main:

.method public hidebysig static void  Main() cil managed
{
  .entrypoint
  // Code size       30 (0x1e)
  .maxstack  1
  .locals init (int32 V_0,
           float64 V_1,
           float64 V_2,
           string V_3)
  IL_0000:  nop
  IL_0001:  ldc.i4.4
  IL_0002:  stloc.0
  IL_0003:  ldc.r8     12.34
  IL_000c:  stloc.1
  IL_000d:  ldc.r8     3.1415926535897931
  IL_0016:  stloc.2
  IL_0017:  ldstr      "Ehsan"
  IL_001c:  stloc.3
  IL_001d:  ret
} // end of method Program::Main

это нормально, и я это понимаю, теперь, если я добавлю еще одну целочисленную переменную, будет сгенерировано что-то другое, вот модифицированный код С#:

using System;

class Program
{
    public static void Main()
    {
    int unassigned;
    int i = 4;
    unassigned = i;
    double d = 12.34;
        double PI = Math.PI;
    string name = "Ehsan";


    }
}

и вот IL, сгенерированный для приведенного выше кода С#:

.method public hidebysig static void  Main() cil managed
{
  .entrypoint
  // Code size       33 (0x21)
  .maxstack  1
  .locals init (int32 V_0,
           int32 V_1,
           float64 V_2,
           float64 V_3,
           string V_4)
  IL_0000:  nop
  IL_0001:  ldc.i4.4
  IL_0002:  stloc.1
  IL_0003:  ldloc.1
  IL_0004:  stloc.0
  IL_0005:  ldc.r8     12.34
  IL_000e:  stloc.2
  IL_000f:  ldc.r8     3.1415926535897931
  IL_0018:  stloc.3
  IL_0019:  ldstr      "Ehsan"
  IL_001e:  stloc.s    V_4  // what is happening here in this case
  IL_0020:  ret
} // end of method Program::Main

Если вы заметили, что теперь оператор stloc.s генерируется с V_4, который является локальным, но я не совсем понимаю это, и я также не понимаю, какова цель этих местных жителей, я имею в виду следующее:

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

person Ehsan Sajjad    schedule 04.12.2015    source источник
comment
stloc.4 нет, поэтому вместо него нужно использовать stloc.s local_variable_reference. я также не понимаю, для чего здесь эти локальные переменные Это ваши локальные переменные int i; double d; double PI; string name;.   -  person user4003407    schedule 04.12.2015
comment
что вы имеете в виду под нет stloc.4 ?   -  person Ehsan Sajjad    schedule 04.12.2015
comment
Есть четыре stloc ярлыка stloc.0, stloc.1, stloc.2 и stloc.3. Для обращения к локальной переменной с индексом 4 или выше вы должны использовать stloc.s или stloc.   -  person user4003407    schedule 04.12.2015
comment
как использовать stloc в случае индекса больше 4   -  person Ehsan Sajjad    schedule 04.12.2015
comment
Во-первых, вы смотрите на отладку IL. Убедитесь, что компилятор C# настроен на оптимизацию без отладки. stloc ldloc это беспроигрышный вариант. Им это нужно для отладки :D   -  person leppie    schedule 04.12.2015


Ответы (2)


Некоторые вещи, чтобы отметить.

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

.method public hidebysig static void Main () cil managed 
{
  .entrypoint

  IL_0000: ret
}

То есть, поскольку эти локальные переменные не используются, я ожидаю, что компилятор просто полностью их пропустит. Этого не будет в отладочной сборке, но это хороший пример того, как может быть значительная разница между тем, что говорит C #, и тем, что говорит IL.

Следующее, на что следует обратить внимание, это структура метода IL. У вас есть массив локальных значений различных типов, определенный блоком .locals. Как правило, они будут очень близко соответствовать тому, что было в C#, хотя часто будут сделаны сокращения и перестановки.

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

Следующее, на что следует обратить внимание, это то, что IL, который вы видите здесь, является своего рода сборкой для байт-кода: каждая инструкция здесь имеет однозначное отображение на один или два байта, и каждое значение также потребляет определенное количество байтов. Так, например, stloc V_4 (на самом деле не присутствует в ваших примерах, но мы придем к этому) будет отображаться в 0xFE 0x0E 0x04 0x00, где 0xFE 0x0E — это кодировка stloc, а 0x04 0x00 — кодировка 4, которая является индексом рассматриваемого локального. Это означает «извлеките значение из вершины стека и сохраните его в 5-м (индекс 4) локальном».

Здесь есть несколько сокращений. Одним из них является .s "короткая" форма нескольких инструкций (_S в названии эквивалентно System.Reflection.Emit.OpCode значению). Это варианты других инструкций, которые принимают однобайтовое значение (со знаком или без знака в зависимости от инструкции), где другая форма принимает двух- или четырехбайтовое значение, как правило, индексы или относительные расстояния для перехода. Таким образом, вместо stloc V_4 у нас может быть stloc.s V_4, которое всего лишь 0x13 0x4, и поэтому меньше.

Затем есть несколько вариантов, которые включают конкретное значение в инструкцию. Таким образом, вместо stloc V_0 или stloc.s V_0 мы можем просто использовать stloc.0, который представляет собой всего лишь один байт 0x0A.

Это имеет большой смысл, если учесть, что обычно одновременно используется только несколько местных жителей, поэтому использование либо stloc.s, либо (еще лучше) подобных stloc.0, stloc.1 и т. д.) дает крошечную экономию, которая в сумме на довольно много.

Но только так. Если бы у нас были, например, stloc.252, stloc.253 и т. д., то таких инструкций было бы много, и количество байтов, необходимых для каждой инструкции, должно было бы быть больше, и это в целом было бы потерей. Суперкороткие формы слов, связанных с локальными (stloc, ldloc) и связанными с аргументами (ldarg), доходят только до 3. (Есть starg и starg.s, но нет starg.0 и т. д., так как сохранение в аргументах встречается относительно редко). ldc.i4/ldc.i4.s (поместить в стек постоянное 32-битное значение со знаком) имеет суперкороткие версии от ldc.i4.0 до ldc.i4.8, а также lcd.i4.m1 для -1.

Также стоит отметить, что V_4 вообще не существует в вашем коде. Что бы вы ни исследовали IL, не знали, что вы использовали имя переменной name, поэтому оно просто использовало V_4. (Кстати, что вы используете? По большей части я использую ILSpy, и если вы хотите отладить информацию, связанную с файл, который он назвал бы name соответственно).

Итак, чтобы создать закомментированную неукороченную версию вашего метода с более сопоставимыми именами, мы могли бы написать следующий CIL:

.method public hidebysig static void  Main() cil managed
{
  .entrypoint
  .maxstack  1
  .locals init (int32 unassigned,
           int32 i,
           float64 d,
           float64 PI,
           string name)
  nop                           // Do Nothing (helps debugger to have some of these around).
  ldc.i4   4                    // Push number 4 on stack
  stloc    i                    // Pop value from stack, put in i (i = 4)
  ldloc    i                    // Push value in i on stack
  stloc    unassigned           // Pop value from stack, put in unassigned (unassigned = i)
  ldc.r8   12.34                // Push the 64-bit floating value 12.34 onto the stack
  stloc    d                    // Push the value on stack in d (d = 12.34)
  ldc.r8   3.1415926535897931   // Push the 64-bit floating value 3.1415926535897931 onto the stack.
  stloc PI                      // Pop the value from stack, put in PI (PI = 3.1415… which is the constant Math.PI)
  ldstr    "Ehsan"              // Push the string "Ehsan" on stack
  stloc    name                 // Pop the value from stack, put in name
  ret                           // return.
}

Это будет вести себя почти так же, как ваш код, но будет немного больше. Поэтому мы заменяем stloc на stloc.0stloc.3 там, где можем, stloc.s там, где мы не можем их использовать, но можем использовать stloc.s, и ldc.i4 4 на ldc.i4.4, и у нас будет более короткий байт-код, который делает то же самое:

.method public hidebysig static void  Main() cil managed
{
  .entrypoint
  .maxstack  1
  .locals init (int32 unassigned,
           int32 i,
           float64 d,
           float64 PI,
           string name)
  nop                           // Do Nothing (helps debugger to have some of these around).
  ldc.i4.4                      // Push number 4 on stack
  stloc.1                       // Pop value from stack, put in i (i = 4)
  ldloc.1                       // Push value in i on stack
  stloc.0                       // Pop value from stack, put in unassigned (unassigned = i)
  ldc.r8   12.34                // Push the 64-bit floating value 12.34 onto the stack
  stloc.2                       // Push the value on stack in d (d = 12.34)
  ldc.r8   3.1415926535897931   // Push the 64-bit floating value 3.1415926535897931 onto the stack.
  stloc.3                       // Pop the value from stack, put in PI (PI = 3.1415… which is the constant Math.PI)
  ldstr    "Ehsan"              // Push the string "Ehsan" on stack
  stloc.s  name                 // Pop the value from stack, put in name
  ret                           // return.
}

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


Ваш вопрос в комментарии действительно должен быть другим вопросом, но он дает возможность добавить что-то важное, что я лишь кратко отметил выше. Давайте рассмотрим:

public static void Maybe(int a, int b)
{
  if (a > b)
    Console.WriteLine("Greater");
  Console.WriteLine("Done");
}

Скомпилируйте в отладке, и вы получите что-то вроде:

.method public hidebysig static 
  void Maybe (
    int32 a,
    int32 b
  ) cil managed 
{
  .maxstack 2
  .locals init (
    [0] bool CS$4$0000
  )

  IL_0000: nop
  IL_0001: ldarg.0
  IL_0002: ldarg.1
  IL_0003: cgt
  IL_0005: ldc.i4.0
  IL_0006: ceq
  IL_0008: stloc.0
  IL_0009: ldloc.0
  IL_000a: brtrue.s IL_0017

  IL_000c: ldstr "Greater"
  IL_0011: call void [mscorlib]System.Console::WriteLine(string)
  IL_0016: nop

  IL_0017: ldstr "Done"
  IL_001c: call void [mscorlib]System.Console::WriteLine(string)
  IL_0021: nop
  IL_0022: ret
}

Теперь следует отметить, что все метки, такие как IL_0017 и т. д., добавляются к каждой строке на основе индекса инструкции. Это облегчает жизнь дизассемблеру, но на самом деле в этом нет необходимости, если только не выполняется переход на метку. Давайте удалим все метки, на которые не перескочили:

.method public hidebysig static 
  void Maybe (
    int32 a,
    int32 b
  ) cil managed 
{
  .maxstack 2
  .locals init (
    [0] bool CS$4$0000
  )

  nop
  ldarg.0
  ldarg.1
  cgt
  ldc.i4.0
  ceq
  stloc.0
  ldloc.0
  brtrue.s IL_0017

  ldstr "Greater"
  call void [mscorlib]System.Console::WriteLine(string)
  nop

  IL_0017: ldstr "Done"
  call void [mscorlib]System.Console::WriteLine(string)
  nop
  ret
}

Теперь давайте рассмотрим, что делает каждая строка:

.method public hidebysig static 
  void Maybe (
    int32 a,
    int32 b
  ) cil managed 
{
  .maxstack 2
  .locals init (
    [0] bool CS$4$0000
  )

  nop                   // Do nothing
  ldarg.0               // Load first argument (index 0) onto stack.
  ldarg.1               // Load second argument (index 1) onto stack.
  cgt                   // Pop two values from stack, push 1 (true) if the first is greater
                        // than the second, 0 (false) otherwise.
  ldc.i4.0              // Push 0 onto stack.
  ceq                   // Pop two values from stack, push 1 (true) if the two are equal,
                        // 0 (false) otherwise.
  stloc.0               // Pop value from stack, store in first local (index 0)
  ldloc.0               // Load first local onto stack.
  brtrue.s IL_0017      // Pop value from stack. If it's non-zero (true) jump to IL_0017

  ldstr "Greater"       // Load string "Greater" onto stack.

                        // Call Console.WriteLine(string)
  call void [mscorlib]System.Console::WriteLine(string)
  nop                   // Do nothing

  IL_0017: ldstr "Done" // Load string "Done" onto stack.
                        // Call Console.WriteLine(string)
  call void [mscorlib]System.Console::WriteLine(string)
  nop                   // Do nothing
  ret                   // return
}

Давайте запишем это обратно в C# очень буквально шаг за шагом:

public static void Maybe(int a, int b)
{
  bool shouldJump = (a > b) == false;
  if (shouldJump) goto IL_0017;
  Console.WriteLine("Greater");
IL_0017:
  Console.WriteLine("Done");
}

Попробуйте это, и вы увидите, что это делает то же самое. Использование goto связано с тем, что в CIL на самом деле нет ничего похожего на for или while или даже блоков, которые мы могли бы поместить после if или else, в нем есть только переходы и условные переходы.

Но почему нужно сохранять значение (то, что я назвал shouldJump в своем переписывании на C#), а не просто воздействовать на него?

Это просто для того, чтобы упростить изучение того, что происходит в каждой точке, если вы отлаживаете. В частности, чтобы отладчик мог остановиться в точке, где a > b обработано, но еще не выполнено действие, необходимо сохранить либо a > b, либо его противоположность (a <= b).

По этой причине сборки отладки, как правило, пишут CIL, который тратит много времени на запись того, что он только что сделал. С релизной сборкой мы получим что-то вроде:

.method public hidebysig static 
  void Maybe (
    int32 a,
    int32 b
  ) cil managed 
{
  ldarg.0           // Load first argument onto stack
  ldarg.1           // Load second argument onto stack
  ble.s IL_000e     // Pop two values from stack. If the first is
                    // less than or equal to the second, goto IL_000e: 
  ldstr "Greater"   // Load string "Greater" onto stack.
                    // Call Console.WriteLine(string)
  call void [mscorlib]System.Console::WriteLine(string)
                    // Load string "Done" onto stack.
  IL_000e: ldstr "Done"
                    // Call Console.WriteLine(string)
  call void [mscorlib]System.Console::WriteLine(string)
  ret
}

Или сделать аналогичную построчную запись обратно в C#:

public static void Maybe(int a, int b)
{
  if (a <= b) goto IL_000e;
  Console.WriteLine("Greater");
IL_000e:
  Console.WriteLine("Done");
}

Таким образом, вы можете видеть, как релизная сборка более лаконично делает то же самое.

person Jon Hanna    schedule 04.12.2015
comment
Не спрашивайте, можете ли вы задать вопрос. Задать вопрос. - person Jon Hanna; 04.12.2015
comment
можете ли вы объяснить мне этот IL, это сравнение if(a > b) в IL можете ли вы объяснить с комментариями, как вы сделали в своем ответе: IL_0001: ldarg.0 IL_0002: ldarg.1 IL_0003: cgt IL_0005: ldc.i4.0 IL_0006: ceq IL_0008: stloc.0 IL_0009: ldloc.0 IL_000a: brtrue.s IL_0019 - person Ehsan Sajjad; 04.12.2015
comment
На самом деле я не могу понять, как работает эта comaprison, предположим, что здесь a больше, чем b. - person Ehsan Sajjad; 04.12.2015
comment
Фактически. Подождите минуту. - person Jon Hanna; 04.12.2015
comment
Там. Надеюсь, это немного объясняет. - person Jon Hanna; 04.12.2015
comment
можете ли вы ответить на этот вопрос: ​​stackoverflow.com/questions/34026958/ - person Ehsan Sajjad; 04.12.2015

MSIL сильно микрооптимизирован, чтобы сделать хранилище как можно меньше. Перейдите к классу Opcodes. и обратите внимание на перечисленные Stloc инструкции. Есть 6 версий, все они делают одно и то же.

Stloc_0, Stloc_1, Stloc_2 и Stloc_3 являются минимальными, они занимают только один байт. Используемый ими номер переменной является неявным, от 0 до 3. Конечно, он используется очень часто.

Затем идет Stloc_S, это двухбайтовый код операции, второй байт для кодирования номера переменной. Это необходимо использовать, когда метод имеет более 4 переменных.

Наконец, есть Stloc, это трехбайтовый код операции, использующий два байта для кодирования номера переменной. Должен использоваться, когда метод имеет более 256 переменных. Надеюсь, ты никогда этого не сделаешь. Вам не повезло, когда вы пишете монстра с более чем 65536 переменными, которые не поддерживаются. Кстати, автоматически сгенерированный код может выйти за этот предел.

Так легко увидеть, что произошло во втором фрагменте, вы добавили переменную unassigned и увеличили количество локальных переменных с 4 до 5. Поскольку Stloc_4 нет, компилятор должен использовать Stloc_S для присвоения 5-го переменная.

person Hans Passant    schedule 04.12.2015
comment
какие-либо мысли по этому поводу: ​​stackoverflow.com/questions/34026958/ - person Ehsan Sajjad; 04.12.2015
comment
Да, это не имеет никакого отношения к вашему вопросу. Просто перейдите по ссылке первого комментария, чтобы увидеть мои мысли. - person Hans Passant; 04.12.2015