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

时间:2012-09-24 20:39:21

标签: c# .net system.reactive

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

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

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

4 个答案:

答案 0 :(得分:3)

  

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

当您调用委托时,它不会调用GetInvocationList。这是用于处理和检查委托的代码,而不是用于执行委托的实际代码。运行时本身实际上在内部执行调用,因为它实际上不在IL中。这在8.9.3的CLI规范中有记录:

  

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

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

CLI规范中的

* VES ==“虚拟执行系统”,这是用于执行运行时本身的代码的术语。

答案 1 :(得分:1)

似乎你在看Rx v1.0实现。在Rx v2.0中对主题实现进行了彻底检查,以避免在调用路径中进行分配和严重锁定。同样,Rx查询管道也经过了修改,并考虑了相同的标准。

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

对于特定主题,如果您连接了2个或更多观察者,则它们通常优于多播代理。在没有附加观察者的情况下,对观察者的虚拟方法调用的成本超过了用于事件的空检查和调用模式。对于一个观察者,我们避免通过调用列表(没有多播部分的〜委托),但虚拟调用的成本仍然显示。随着附加的观察者越来越多,我们的虚拟方法的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);
}

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

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

简而言之:如果您决定进行任何类型的测试,请转到Rx v2.0。性能是此版本的第一个特性: - )。

答案 2 :(得分: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);
            }
        }
    }
}

答案 3 :(得分:0)

关于Rx和分配; Rx v2.0投入了大量精力来减少执行的分配和锁定次数。 V1是一款出色的产品,但已被证明可以在API的公共表面上获得2年的行业反馈。一旦这一点变得清晰,业界需要什么,Rx团队就离开了,并在内部工作。从我看到的情况来看,Bart DeSmet能够在使用Rx的64路系统上获得超过90-95%的CPU利用率。这是一个指示它是CPU绑定而不是上下文切换,锁定或IO绑定(我不能在我的生活中找到显示它的帖子)。即系统完全利用处理Rx查询而不处理其他管道。

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

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