Почему компилятор добавляет дополнительный параметр для делегатов, когда нет закрытия?

Я играл с delegates и заметил, что когда я создаю Func<int,int,int>, как показано ниже:

Func<int, int, int> func1 = (x, y) => x * y;

Подпись метода сгенерированного компилятора не соответствует ожидаемому:

enter image description here

Как вы можете видеть, для него первый объект принимает объект. Но когда есть замыкание:

int z = 10;
Func<int, int, int> func1 = (x, y) => x * y * z;

Everthing работает как ожидалось:

enter image description here

Это код IL для метода с дополнительным параметром:

    .method private hidebysig static int32  '<Main>b__0'(object A_0,
                                                     int32 x,
                                                     int32 y) cil managed
{
  .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) 
  // Code size       8 (0x8)
  .maxstack  2
  .locals init ([0] int32 V_0)
  IL_0000:  ldarg.1
  IL_0001:  ldarg.2
  IL_0002:  mul
  IL_0003:  stloc.0
  IL_0004:  br.s       IL_0006
  IL_0006:  ldloc.0
  IL_0007:  ret
} // end of method Program::'<Main>b__0'

Кажется, что параметр A_0 даже не используется. Итак, какова цель параметра object в первом случае? Почему он не добавляется при закрытии?

Примечание. Если у вас есть идея, лучше редактировать.

Примечание 2: Я скомпилировал первый код в режимах Debug и Release, не было никакой разницы. Но я скомпилировал второй в режиме Debug, чтобы получить поведение закрытия, так как он оптимизирует локальную переменную в режиме Release.

Примечание 3: Я использую Visual Studio 2014 CTP.

Изменить: Это сгенерированный код для Main в первом случае:

.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // Code size       30 (0x1e)
  .maxstack  2
  .locals init ([0] class [mscorlib]System.Func`3<int32,int32,int32> func1)
  IL_0000:  nop
  IL_0001:  ldsfld     class [mscorlib]System.Func`3<int32,int32,int32> ConsoleApplication9.Program::'CS$<>9__CachedAnonymousMethodDelegate1'
  IL_0006:  dup
  IL_0007:  brtrue.s   IL_001c
  IL_0009:  pop
  IL_000a:  ldnull
  IL_000b:  ldftn      int32 ConsoleApplication9.Program::'<Main>b__0'(object,
                                                                       int32,
                                                                       int32)
  IL_0011:  newobj     instance void class [mscorlib]System.Func`3<int32,int32,int32>::.ctor(object,
                                                                                             native int)
  IL_0016:  dup
  IL_0017:  stsfld     class [mscorlib]System.Func`3<int32,int32,int32> ConsoleApplication9.Program::'CS$<>9__CachedAnonymousMethodDelegate1'
  IL_001c:  stloc.0
  IL_001d:  ret
} // end of method Program::Main

Ответ 1

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

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

Предположим, вы вызываете func1(1, 2). Это выглядит как (псевдокод, а не CIL)

push func1
push 1
push 2
call Func<,,>::Invoke

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

push arg.1
push arg.2
call method

или

arg.0 = arg.1
arg.1 = arg.2
jmp method

В то время, когда известно, что func1 связано со статической функцией, принимающей null и двумя значениями int, она должна выполнять только эквивалент

arg.0 = null
jmp method

так как среда уже настроена идеально для ввода функции, использующей ссылочный тип и два значения int.

Да, это микро-оптимизация, которая, как правило, не имеет значения, но она доступна всем, в том числе в ситуациях, когда это имеет значение.