调用多播委托是否分配内存



我们正在考虑重新设计一些接口,以使用IObservable、Subject和Observer(后两者在Reactive扩展中),而不是标准的.NET事件。在查看实现后,我们发现Subject将锁定需要回调的IObserver列表,然后创建一个新数组,将IObserver引用复制到该新数组中,然后调用它们。

当我们看到MulticastDelegate的实现时,我们可以看到Multicastdelete.GetInvocationList的实现还创建了一个新的数组,将要调用的委托复制到该数组中,然后调用它们。尚不清楚的是,当您调用多播委托时,是否会调用GetInvocationList,或者是否在框架内以不分配内存的方式处理它。调用多播委托会分配一个新数组吗?或者,框架是否会处理一些事情,从而在引发事件时不会分配新的数组?

我们的应用程序对内存分配和延迟非常敏感,因此我们试图通过转移到新接口来确保不会在事件调用中分配更多内存。我们也将在内部运行一些测试。

不清楚的是,当您调用多播委托时,是否会调用GetInvocationList,或者是否在框架内以不分配内存的方式处理它。

调用委托时,它不会调用GetInvocationList。这是用于处理和检查委托的代码,而不是用于执行委托的实际代码。运行时本身实际上在内部执行调用,因为它实际上不在IL中

虽然在大多数情况下,委托似乎只是另一种用户定义的类,但它们受到严格控制。这些方法的实现是由VES*提供的,而不是用户代码。

基本上,实际调用由运行时内部处理。

 *CLI规范中的VES=="虚拟执行系统",用于执行运行时本身的代码。

您似乎正在查看Rx v1.0的实现。Rx v2.0中对主题实现进行了彻底检修,以避免调用路径中的分配和重锁定。类似地,Rx查询管道也考虑到了相同的标准进行了修订。

请参阅http://blogs.msdn.com/b/rxteam/archive/2012/03/12/reactive-extensions-v2-0-beta-available-now.aspx有关Rx v2.0性能改进的更多信息。(虽然这篇文章可以追溯到测试版,但大多数信息都适用于RTM构建。为了更好,有一些东西已经改进了。)

特别是对于受试者,如果你有2个或更多的观察者,他们通常会优于多播代理。在没有附加观察者的情况下,虚拟方法调用观察者的成本超过了用于事件的null检查和调用模式。使用一个观察者,我们可以避免遍历调用列表(~delegate没有多播部分),但虚拟调用的成本仍然会显示出来。有了更多的观察者,我们的foreach虚拟方法循环往往比多播委托背后的机制更快。

以下是我们直接从Rx测试中提取的基准代码的一个稍微简化的摘录(使一些参数保持不变并删除对内部类型的引用):

var e = default(Action<int>);
var a = new Action<int>(_ => { });
var s = new Subject<int>();
var n = new NopObserver<int>();
var N = 20;
var M = 10000000;
var sw = new Stopwatch();
for (int i = 0; i < N; i++)
{
sw.Restart();
for (int j = 0; j < M; j++)
{
var f = e;
if (f != null)
f(42);
}
sw.Stop();
var t = sw.Elapsed;
Console.WriteLine("E({0}) = {1}", i, t);
sw.Restart();
for (int j = 0; j < M; j++)
{
s.OnNext(42);
}
sw.Stop();
var u = sw.Elapsed;
Console.WriteLine("O({0}) = {1}", i, u);
var d = u.TotalMilliseconds / t.TotalMilliseconds;
Console.ForegroundColor = d <= 1 ? ConsoleColor.Green : ConsoleColor.Red;
Console.WriteLine(d + " - " + GC.CollectionCount(0));
Console.ResetColor();
Console.WriteLine();
e += a;
s.Subscribe(n);
}

在这台机器上,前两次迭代变为红色;随后的迭代(处理程序计数>=2)显示出20%-35%的加速。所有关于基准测试的常见警告都适用:-)。

此外,请记住,观察者包装的开销(为了安全保证)会随着Rx中的管道变长而降低。这是因为Rx v2.0在受信任的操作员之间执行内部握手,避免了额外的包装。只有最终的用户订阅才会受到Rx和用户提供的观察者代码之间的另一层虚拟呼叫的影响,以确保正确的异常传播等。在Rx v1.0中,为每个运营商提供了一个安全网,为流经每个运营商的每个消息添加2到4个额外的虚拟呼叫。

简而言之:如果您决定进行任何类型的测试,请选择Rx v2.0。性能是此版本的首要功能:-)。

好吧,在玩了下面的测试应用程序之后,我确信调用MulticastDelegate不会分配托管内存。如果有人知道不同,请告诉我。


using System;
using System.Diagnostics;
internal class Program
{
private static event Action A;
private static void Method1() {}
private static void Method2() {}
private static void Method3() {}
private static void Main()
{
A += Method1;
A += Method2;
A += Method3;
var totalMemory = GC.GetTotalMemory(true);
while(true)
{
A();
// Uncommenting the line below will cause the Debug.Assert to be hit.
// var a = new int[] {};
if (totalMemory != GC.GetTotalMemory(false))
{
// Does not get hit unless line above allocating an array is
// uncommented.
Debug.Assert(false);
}
}
}
}

关于Rx和分配;Rx v2.0投入了大量精力来减少执行的分配和锁定数量。V1是一个伟大的产品,但事实证明,它是在API的公共表面上获得2年行业反馈的地方。一旦这一点清楚了行业需要什么,Rx团队就离开了,并着手进行内部工作。据我所见,Bart DeSmet能够在使用Rx的64路系统上获得超过90-95%的CPU利用率。这表明它是CPU绑定的,而不是上下文切换、锁定或IO绑定的(然而,我一生都找不到显示它的帖子)。即,该系统完全用于处理Rx查询而不处理其他管道。

根据我使用Rx的经验,还有很多其他事情可以让你措手不及,分配通常不是其中之一。虽然我理解对低延迟系统的要求,并且分配可能会导致GC抖动,但我认为您将构建一个单线程管道。如果没有,我预计多线程应用程序的上下文切换将远远超过GC的成本。在这种单线程管道的情况下,您将获得的主要好处是能够组合Rx运算符来构建比处理事件更可读的查询。

最后,我要避免在代码中使用subject或Observer类。这样做表明存在设计缺陷。如果你发现你确实开始使用它们,我建议在这里分享你的问题空间(或者在官方Rx论坛上更好http://social.msdn.microsoft.com/Forums/en-US/rx/threads)社区可以提供更好的指导。

最新更新