Roslyn 中的代理缓存行为更改
Delegate caching behavior changes in Roslyn
给定以下代码:
public class C
{
public void M()
{
var x = 5;
Action<int> action = y => Console.WriteLine(y);
}
}
使用 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);
}
}
查看在 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);
}
}
}
我们现在可以看到委托现在被提升到 C
内部的私有 class,这是我们在关闭实例变量/字段时看到的类似行为(闭包).
我知道这是一个实施细节,可能会随时更改。
我仍然想知道,将委托提升到一个新的 class 并将其缓存在那里比简单地在调用站点缓存它有什么好处?
编辑:
This issue 讨论与此处询问的相同行为。
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?
您错过了另一个 真正 重要的细节 - 它现在是一个实例方法。我相信这是这里的关键。 IIRC,发现通过实例方法调用 "backed" 的委托比调用由静态方法支持的委托更快 - 这是更改背后的动机。
这都是道听途说,在 CodeMash 与 Dustin Campbell 和 Kevin Pilch-Bisson(均来自 Roslyn 团队)共度时光时依稀记得,但考虑到您展示的代码,这是有道理的。
(我还没有为自己验证性能差异,听起来好像是倒退了...但是 CLR 内部可能很有趣...)
是的。最重要的部分是包含 lambda 实现的方法现在是一个实例方法。
您可以将委托视为中间人,通过 Invoke 接收实例调用并根据实现方法的调用约定调度该调用。
请注意,平台 ABI 要求指定如何传递参数、如何返回结果、通过寄存器传递哪些参数以及在哪些寄存器中传递参数、如何传递 "this" 等等。违反这些规则可能会对依赖堆栈遍历的工具(例如调试器)产生不良影响。
现在,如果实现方法是一个实例方法,委托内部唯一需要做的就是修补"this",即Invoke时的委托实例,作为封装目标对象。到那时,由于其他一切都已经在需要的地方,委托可以直接跳转到实现方法体。在许多情况下,如果实现方法是静态方法,这明显比需要发生的工作要少。
给定以下代码:
public class C
{
public void M()
{
var x = 5;
Action<int> action = y => Console.WriteLine(y);
}
}
使用 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);
}
}
查看在 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);
}
}
}
我们现在可以看到委托现在被提升到 C
内部的私有 class,这是我们在关闭实例变量/字段时看到的类似行为(闭包).
我知道这是一个实施细节,可能会随时更改。
我仍然想知道,将委托提升到一个新的 class 并将其缓存在那里比简单地在调用站点缓存它有什么好处?
编辑:
This issue 讨论与此处询问的相同行为。
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?
您错过了另一个 真正 重要的细节 - 它现在是一个实例方法。我相信这是这里的关键。 IIRC,发现通过实例方法调用 "backed" 的委托比调用由静态方法支持的委托更快 - 这是更改背后的动机。
这都是道听途说,在 CodeMash 与 Dustin Campbell 和 Kevin Pilch-Bisson(均来自 Roslyn 团队)共度时光时依稀记得,但考虑到您展示的代码,这是有道理的。
(我还没有为自己验证性能差异,听起来好像是倒退了...但是 CLR 内部可能很有趣...)
是的。最重要的部分是包含 lambda 实现的方法现在是一个实例方法。
您可以将委托视为中间人,通过 Invoke 接收实例调用并根据实现方法的调用约定调度该调用。
请注意,平台 ABI 要求指定如何传递参数、如何返回结果、通过寄存器传递哪些参数以及在哪些寄存器中传递参数、如何传递 "this" 等等。违反这些规则可能会对依赖堆栈遍历的工具(例如调试器)产生不良影响。
现在,如果实现方法是一个实例方法,委托内部唯一需要做的就是修补"this",即Invoke时的委托实例,作为封装目标对象。到那时,由于其他一切都已经在需要的地方,委托可以直接跳转到实现方法体。在许多情况下,如果实现方法是静态方法,这明显比需要发生的工作要少。