3

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

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

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

4

4 回答 4

3

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

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

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

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

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

于 2012-09-24T20:50:50.373 回答
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。(虽然这篇文章是从测试版开始的,但大多数信息都适用于 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。性能是此版本的第一大功能:-)。

于 2012-10-05T09:29:24.133 回答
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);
            }
        }
    }
}
于 2012-09-24T21:26:40.957 回答
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上更好)和社区可以提供更好的指导。

于 2012-10-03T10:21:25.477 回答