【问题标题】:Does invoking a multicast delegate allocate memory?调用多播委托是否分配内存?
【发布时间】:2012-09-24 20:39:20
【问题描述】:

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

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

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

【问题讨论】:

  • 如果您的应用对少量分配敏感,那么您将遇到其他问题。
  • 这一特殊事件每秒可引发数千次。虽然单个分配不会对我们的应用程序造成问题,但每秒可能会有数千个小分配。
  • @Dylan 您是否使用分析器执行了代码并确定这会消耗大量系统资源?在寻找解决方案之前,请确保您确实遇到了问题。
  • @Servy - 是的,此代码位于我们应用程序的热门路径之一。这是非常时间关键的代码。我们正在评估不同的方法,因此还不能对每种方法进行分析。
  • “我们正在评估不同的方法,因此还不能对每种方法进行分析。” - 这不是本末倒置吗?!?

标签: c# .net system.reactive


【解决方案1】:

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

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

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

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

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

【讨论】:

  • 明白了——说得通。我假设调用 delgate 时运行时不会分配内存。我的假设是否正确?
  • 为了让我之前的评论更加具体——我假设运行时在调用 delgate 时不会分配托管内存(例如,新的对象数组)。如果它在内部分配内存,那很好。我更关心它是如何分配与 System.Reactive 扩展相关的。
  • @Dylan 运行时在执行期间不会分配正常的托管数组。它的内部实现可以随心所欲地做任何事情,但它是“严格控制”的,而不是在托管空间中分配数组。
【解决方案2】:

您似乎正在查看 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。 (虽然这篇文章是从测试版开始的,但大部分信息都适用于 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);
}

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

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

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

【讨论】:

    【解决方案3】:

    好的,在玩过下面的测试应用程序之后,我确信调用 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);
                }
            }
        }
    }
    

    【讨论】:

      【解决方案4】:

      关于 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),社区可以提供更好的指导。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2011-01-12
        • 2014-11-01
        • 1970-01-01
        • 2022-01-17
        • 2016-10-25
        • 2011-09-11
        相关资源
        最近更新 更多