Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Does casting an anonymous lambda to a strongly typed delegate disable compiler caching?

I'm trying to understand an edge-case of compiler powered delegate caching to avoid memory allocation.

For example, to my understanding, this delegate gets cached to a single instance and reused, because it doesn't close over any local variable:

int[] set = new [] { 1, 2, 3, 4, 5, 6 };
var subset = set.Where(x => x % 2 == 0);

Now I have some cases where my generated code might want to invoke a delegate directly, so an anonymous method is not valid C#, like so:

var result = (x => x % 2 == 0).Invoke(5); // Invalid

To circumvent this, I see two options:

  1. Using the constructor:
var result = (new Func<int, bool>(x => x % 2 == 0)).Invoke(5);
  1. Casting the anonymous delegate:
var result = ((Func<int, bool>)(x => x % 2 == 0)).Invoke(5);

I'm assuming the compiler will not cache the delegate in option #1, but I'm not sure if it will in #2.

Is this documented anywhere?

like image 994
Lazlo Avatar asked Jan 26 '23 13:01

Lazlo


1 Answers

I'm assuming the compiler will not cache the delegate in option #1, but I'm not sure if it will in #2.

In fact it can in both cases, and they're tied together.

From the ECMA C# 5 spec, section 7.6.10.5:

The binding-time processing of a delegate-creation-expression of the form new D(E), where D is a delegate-type and E is an expression, consists of the following steps:

  • ...
  • If E is an anonymous function, the delegate creation expression is processed in the same way as an anonymous function conversion (§6.5) from E to D.
  • ...

So basically the two are handled in the same way. And in both cases it can be cached. And yes, it's very odd that "new doesn't necessarily mean new".

To show this, let's write a pretty trivial program:

using System;

public class Program
{
    public static void Main()
    {
        var func = new Func<int, bool>(x => x % 2 == 0);
    }
}

Here's the IL for the Main method on my machine (admittedly building with the C# 8 preview compiler, but I'd expect the same for a while):

.method public hidebysig static void  Main() cil managed
{
  .entrypoint
  // Code size       29 (0x1d)
  .maxstack  8
  IL_0000:  ldsfld     class [mscorlib]System.Func`2<int32,bool> Program/'<>c'::'<>9__0_0'
  IL_0005:  brtrue.s   IL_001c
  IL_0007:  ldsfld     class Program/'<>c' Program/'<>c'::'<>9'
  IL_000c:  ldftn      instance bool Program/'<>c'::'<Main>b__0_0'(int32)
  IL_0012:  newobj     instance void class [mscorlib]System.Func`2<int32,bool>::.ctor(object,
                                                                                      native int)
  IL_0017:  stsfld     class [mscorlib]System.Func`2<int32,bool> Program/'<>c'::'<>9__0_0'
  IL_001c:  ret
} // end of method Program::Main

That's effectively:

Func<int, bool> func;
func = cache;
if (func == null)
{
    func = new Func<int, bool>(GeneratedPrivateMethod);
    cache = func;
}
like image 157
Jon Skeet Avatar answered Jan 29 '23 02:01

Jon Skeet