25

I was playing with delegates and noticed that when I create a Func<int,int,int> like the example below:

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

The signature of the compiler generated method is not what I expected:

enter image description here

As you can see it takes an object for it's first parameter. But when there is a closure:

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

Everything works as expected:

enter image description here

This is the IL code for the method with extra parameter:

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

It seems that the parameter A_0 is not even used. So, what is the purpose of the object parameter in the first case? Why isn't it added when there is a closure?

Note: If you have a better idea for the title please feel free to edit.

Note 2: I compiled the first code in both Debug and Release modes, there was no difference. But I compiled second in Debug mode to get a closure behaviour since it optimizes the local variable in Release mode.

Note 3: I'm using Visual Studio 2014 CTP.

Edit: This is the generated code for Main in the first case:

.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
Selman Genç
  • 100,147
  • 13
  • 119
  • 184
  • When you call it, what gets passed in? – siride Dec 31 '14 at 20:14
  • @siride in the code, everything works as expected. I don't see any extra parameter in the `func`'s signature when calling it – Selman Genç Dec 31 '14 at 20:16
  • 1
    If there really is an object parameter, then when the lambda is called, something should be passed in that slot. It should at least appear in the IL. That's where you should look. – siride Dec 31 '14 at 20:17
  • it passes null as far as I can read the code correctly – Selman Genç Dec 31 '14 at 20:19
  • Well now we just have to wait for Jon Skeet or Eric Lippert to show up. – siride Dec 31 '14 at 20:20
  • It would seem the object is included to be used as the return value when the type isn't obvious from the right hand side of the expression? Just speculation. – Travis J Dec 31 '14 at 20:20
  • @TravisJ: unlikely, since the types have to be resolved at compile time. Also, this is a parameter, not a return type. – siride Dec 31 '14 at 20:22
  • @siride I took another look at the code and it seems the null is passed to the constructor of Func. not the method since I'm not calling it at all. – Selman Genç Dec 31 '14 at 20:23
  • @Selman22: I thought you were calling it? If you're not, put in a call to the lambda somewhere and look at what IL is generated by that call to see what it's actually passing for that parameter. – siride Dec 31 '14 at 20:25
  • 1
    I compiled the same code (at least the snippets that you pasted here) and it doesn't generate that extra parameter for me. VS2013, both .NET 3.5 and 4.5.1. – fejesjoco Dec 31 '14 at 20:26
  • @siride yes I just did it, and it calls the Func's Invoke method by passing two parameters, no mention of the first parameter. – Selman Genç Dec 31 '14 at 20:27
  • Show us the IL code that actually calls your lambda. Or tell us more about your compiler settings and exact source code so that we could reproduce the same behavior. – fejesjoco Dec 31 '14 at 20:30
  • @fejesjoco I'm using Visual Studio 2014 CTP, that might be relevant. I'm gonna include it to question – Selman Genç Dec 31 '14 at 20:30
  • My guess then, this is a bug in VS2014 CTP. I'm also guessing that a null is passed when it is called. – fejesjoco Dec 31 '14 at 20:35
  • @fejesjoco your first guess might be correct, but second is not because I checked. it just calls Func's Invoke method and passes two int. – Selman Genç Dec 31 '14 at 20:39
  • 1
    It might just be for simplicitly, since it's harmless. This way, the first argument is always loaded with `ldarg_1`, regardless of whether it's implemented as a static or a non-static method. –  Dec 31 '14 at 20:40
  • @Selman22 I doubt you can call a 3-argument lambda with just 2 arguments. Something must be there on the stack before the two ints. – fejesjoco Dec 31 '14 at 20:46
  • @fejesjoco Yes, and that something is `null`. Remember that a delegate can be bound to an instance method, where it also holds the value to pass as `this`. It's actually done more generically than that, and a delegate can equally hold the first explicit argument to pass to a static method. –  Dec 31 '14 at 20:51
  • I see there is c___Display() class in the second case , why ? – niceman Dec 31 '14 at 20:54
  • @niceman after 4 years you probably already forgot about this question :) anyways, it's the compiler generated class for the closure, so it can hold the local variable `z`. If you had noticed `z` is a public field of that class. – Selman Genç Aug 27 '18 at 01:18

1 Answers1

18

Although this may seem highly surprising, a quick search shows that it's for performance reasons.

On a bug report about it, it's pointed out that delegates to that have no implicit this are measurably slower than delegates that do have an implicit this, because delegates that don't have an implicit this need to do a bit of complicated argument shuffling whenever the delegate is invoked:

Suppose you call func1(1, 2). This looks like (pseudo-code, not CIL)

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

When this func1 is known to be bound to a static function taking two int values, it then needs to perform the equivalent of either

push arg.1
push arg.2
call method

or

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

Whereas when func1 is known to be bound to a static function taking null and two int values, it only needs to perform the equivalent of

arg.0 = null
jmp method

since the environment is already set up perfectly for entering a function taking a reference type and two int values.

Yes, it's a micro-optimisation that typically won't matter, but it's one that everyone benefits from, including those in situations where it does matter.

  • 3
    How many times have I read here that micro-optimizations don't matter :). I never agree. I find this quite nice. – fejesjoco Dec 31 '14 at 20:55
  • good answer, but you may be clarify the _complicated argument shuffling_ part a bit for future readers, I get it when I read the comments. :) – Selman Genç Dec 31 '14 at 21:20
  • @Selman22 Something like this? If you think I misrepresented anything here, please do comment. –  Dec 31 '14 at 21:37
  • One also just have to ask if this just a small missed optimization by the JIT, that seemingly went unnoticed for more than a decade ;p – leppie Dec 31 '14 at 21:51
  • yes this is what I meant.one question, by this _environment is already set up perfectly for entering a function taking a reference type and two int values._ do you mean that the arguments are already pushed to the registers like when calling an instance method and no need to mess with them. and this way the calling convention that is used for instance methods can be used with static method too just by setting first argument to null. did I get it correct? – Selman Genç Dec 31 '14 at 22:25
  • @Selman22 Yes, even when you don't look at C#, OOP calling conventions very often say the `this` parameter of instance methods is passed as if calling a non-instance method with `this` prepended to the rest of the arguments. There's also the fact that extension methods can be bound to delegates just like instance methods (e.g. `public static int Add(this string s, int a, int b)` can be used in `Func f = "abc".Add;`), which is a lot easier to get working if the calling conventions are compatible. –  Dec 31 '14 at 22:43
  • 3
    A higher level view of it that is language agnostic: This is a micro-optimization because `Func`'s call operator takes `Func, int, int` -- the first being the "pointer-to-`Func<>`", the `this` of the type-erasing function storer. If we mangle the stored method to have the same signature (`object, int, int` instead of `int, int`), you can do the equivalent of tail-call recursion forwarding (just bounce the arguments over). In short, the optimization depends on the calling convention. – Yakk - Adam Nevraumont Jan 01 '15 at 03:08