委托Roslyn中的缓存行为更改

Yuv*_*kov 30 .net c# compiler-construction roslyn c#-6.0

给出以下代码:

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

使用VS2013,.NET 4.5.查看反编译代码时,我们可以看到编译器在调用站点缓存委托:

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

查看在Roslyn中反编译的相同代码(使用TryRoslyn),产生以下输出:

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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

我们现在可以看到委托现在被提升到一个私有类里面C,这是我们习惯在关闭实例变量/字段(闭包)时看到的类似行为.

我知道这是一个实施细节,可能会在任何特定时间发生变化.

我仍然想知道,将代理提升到一个新类并在那里缓存它只是在调用站点缓存它有什么好处?

编辑:

此问题涉及与此处要求的相同行为.

VSa*_*dov 29

是.最重要的部分是包含lambda实现的方法现在是一个实例方法.

您可以看到委托作为中间人通过Invoke接收实例调用,并根据实现方法的调用约定调度该调用.

请注意,有平台ABI要求指定参数的传递方式,返回结果的方式,通过寄存器传递的参数以及传递的"this"等等.违反这些规则可能会对依赖堆栈行走的工具(如调试器)产生不良影响.

现在,如果实现方法是一个实例方法,那么代理中唯一需要发生的事情就是将"this"(它是Invoke时的委托实例)修补为封闭的Target对象.此时,由于其他所有内容已经存在,所以委托可以直接跳转到实现方法体.在许多情况下,如果实现方法是静态方法,那么工作量明显少于需要的工作量.

  • CPU的当前实现更容易.实施起来并不容易:-).除了调用站点缓存之外,还有一个单例显示类实例要处理.有一个静态构造函数.没有多少额外的,但肯定有更多的箍要跳过. (4认同)
  • @YuvalItzchakov - 如果问题是"为什么原始实现使用静态方法" - 我不确定.我听说代表们最初的实施并不是很聪明或者不是真的,而且随着时间的推移它们都得到了改进.如果情况确实如此,那么最初的实例/静态可能没有产生足够的差异,并且团队采用了更容易实现的选择. (2认同)

Jon*_*eet 18

我仍然想知道,将代理提升到一个新类并在那里缓存它只是在调用站点缓存它有什么好处?

你错过了另一个非常重要的细节 - 它现在是一个实例方法.我相信这是关键所在.IIRC,发现调用由实例方法"支持"的委托比调用静态方法支持的委托更快 - 这是变更背后的动机.

这一切都是传闻,模糊地记得花时间与CodeMash的Dustin Campbell和Kevin Pilch-Bisson(都来自Roslyn团队),但考虑到你所展示的代码,它会有意义.

(我没有验证自己的性能差异,听起来像是倒退......但CLR内部结构可能很有趣......)

  • 它更快的原因是因为委托调用针对实例方法进行了优化,并且在堆栈上为它们提供了空间.要调用静态方法,他们必须改变参数. (19认同)
  • @Kevin有趣.我们可以在任何地方阅读这种行为?也许更深入的东西? (3认同)
  • @YuvalItzchakov毫秒是错误的时间单位.你要说的是纳秒,而不是几毫秒. (2认同)