Delegate caching behavior changes in Roslyn

Given the following code:

public class C
{
    public void M()
    {
        var x = 5;
        Action<int> action = y => Console.WriteLine(y);
    }
}

Using VS2013, .NET 4.5. When looking at the decompiled code, we can see that the compiler is caching the delegate at the call site:

public class C
{
    [CompilerGenerated]
    private static Action<int> CS$<>9__CachedAnonymousMethodDelegate1;
    public void M()
    {
        if (C.CS$<>9__CachedAnonymousMethodDelegate1 == null)
        {
            C.CS$<>9__CachedAnonymousMethodDelegate1 = new Action<int>(C.<M>b__0);
        }
        Action<int> arg_1D_0 = C.CS$<>9__CachedAnonymousMethodDelegate1;
    }
    [CompilerGenerated]
    private static void <M>b__0(int y)
    {
        Console.WriteLine(y);
    }
}

Looking at the same code decompiled in Roslyn (using TryRoslyn), yields the following output:

public class C
{
    [CompilerGenerated]
    private sealed class <>c__DisplayClass0
    {
        public static readonly C.<>c__DisplayClass0 CS$<>9__inst;
        public static Action<int> CS$<>9__CachedAnonymousMethodDelegate2;
        static <>c__DisplayClass0()
        {
            // Note: this type is marked as 'beforefieldinit'.
            C.<>c__DisplayClass0.CS$<>9__inst = new C.<>c__DisplayClass0();
        }
        internal void <M>b__1(int y)
        {
            Console.WriteLine(y);
        }
    }
    public void M()
    {
        Action<int> arg_22_0;
        if (arg_22_0 = C.<>c__DisplayClass0.CS$<>9__CachedAnonymousMethodDelegate2 == null)
        {
            C.<>c__DisplayClass0.CS$<>9__CachedAnonymousMethodDelegate2 =
                            new Action<int>(C.<>c__DisplayClass0.CS$<>9__inst.<M>b__1);
        }
    }
}

We can now see that the delegate is now lifted into a private class inside C, a similar behavior that we're used to seeing when closing over an instance variable / field (closure).

I know this is an implementation detail which may be subject to change at any given time.

Still I wonder, what are the benefits of lifting the delegate into a new class and caching it there over simply caching it at the call site?

Edit:

This issue talks about the same behavior as asked here.

Jon Skeet
people
quotationmark

Still I wonder, what are the benefits of lifting the delegate into a new class and caching it there over simply caching it at the call site?

You've missed one other really important detail - it's now an instance method. I believe that's the key here. IIRC, it was found that invoking a delegate which was "backed" by an instance method was faster than invoking a delegate backed by a static method - which is the motivation behind the change.

This is all hearsay, vaguely remembered from spending time with Dustin Campbell and Kevin Pilch-Bisson (both from the Roslyn team) at CodeMash, but it would make sense given the code you've shown.

(I haven't validated the performance difference for myself, and it sounds like it's backwards... but CLR internals can be funny like that...)

people

See more on this question at Stackoverflow