Почему этот очень простой метод С# создает такой нелогичный код CIL?

Недавно я копал в IL, и я заметил некоторое нечетное поведение компилятора С#. Следующий метод является очень простым и проверяемым приложением, он немедленно выйдет с кодом выхода 1:

static int Main(string[] args)
{
    return 1;
}

Когда я компилирую это с помощью сообщества Visual Studio 2015, генерируется следующий код IL (добавлены комментарии):

.method private hidebysig static int32 Main(string[] args) cil managed
{
  .entrypoint
  .maxstack  1
  .locals init ([0] int32 V_0)     // Local variable init
  IL_0000:  nop                    // Do nothing
  IL_0001:  ldc.i4.1               // Push '1' to stack
  IL_0002:  stloc.0                // Pop stack to local variable 0
  IL_0003:  br.s       IL_0005     // Jump to next instruction
  IL_0005:  ldloc.0                // Load local variable 0 onto stack
  IL_0006:  ret                    // Return
}

Если бы я должен был передать этот метод, казалось бы, тот же результат можно было бы достичь со следующим IL:

.method static int32 Main()
{
  .entrypoint
  ldc.i4.1               // Push '1' to stack
  ret                    // Return
}

Существуют ли основополагающие причины, по которым я не знаю, что это ожидаемое поведение?

Или просто, что собранный объектный код IL дополнительно оптимизирован по линии, поэтому компилятору С# не нужно беспокоиться об оптимизации?

Ответ 1

Выведенный вывод предназначен для сборки отладки. С выпуском release (или в основном с включенными оптимизациями) компилятор С# генерирует тот же IL, который вы бы написали вручную.

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

Мораль: когда вы хотите запустить оптимизированный код, убедитесь, что вы не просите компилятор генерировать код, предназначенный для отладки :)

Ответ 2

Ответ Джона, конечно, правильный; этот ответ должен следить за этим комментарием:

@EricLippert локальный имеет смысл, но есть ли какое-либо обоснование для этой инструкции br.s, или это просто из-за удобства в коде эмиттера? Я предполагаю, что если компилятор захочет вставить место для точки останова там, он может просто испустить nop...

Причина для кажущейся бессмысленной ветки становится более разумной, если вы посмотрите на более сложный фрагмент программы:

public int M(bool b) {
    if (b) 
      return 1; 
    else 
      return 2;
}

Неоптимизированный ИЛ

    IL_0000: nop
    IL_0001: ldarg.1
    IL_0002: stloc.0
    IL_0003: ldloc.0
    IL_0004: brfalse.s IL_000a
    IL_0006: ldc.i4.1
    IL_0007: stloc.1
    IL_0008: br.s IL_000e
    IL_000a: ldc.i4.2
    IL_000b: stloc.1
    IL_000c: br.s IL_000e
    IL_000e: ldloc.1
    IL_000f: ret

Обратите внимание, что есть два оператора return но только одна команда ret. В неоптимизированном ИЛ, шаблон для кодирования простого оператора возврата:

  • введите значение, которое вы собираетесь вернуть в слот стека
  • ветвь/оставить до конца метода
  • в конце метода, прочитайте значение из слота и верните

То есть в неоптимизированном коде используется форма с одной точкой возврата.

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

Ответ 3

То, что я собираюсь написать, на самом деле не является специфичным для.NET, но общим, и я не знаю оптимизаций, которые.NET распознает и использует при создании CIL. Дерево синтаксиса (а также сам анализатор грамматики) распознает оператор return со следующими лексемами:

returnStatement ::= RETURN expr ;

где returnStatement и expr не являются терминалами, а RETURN - терминалом (return token), поэтому при посещении узла для константы 1 парсер ведет себя так, как если бы он являлся частью выражения. Чтобы еще раз иллюстрировать, что я имею в виду, код для:

return 1 + 1;

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

push const_1 // Pushes numerical value '1' to expression stack
push const_1 // Pushes numerical value '1' to expression stack
add          // result = pop() + pop(); push(result)
return       // pops the value on the top of the stack and returns it as the function result
exit