为什么IEnumerable.ToObservable这么慢?

时间:2020-04-02 08:42:18

标签: c# system.reactive rx.net

我尝试一次枚举一个大的IEnumerable,并观察带有各种运算符(CountSumAverage等)的枚举。一种明显的方法是使用方法ToObservable将其转换为IObservable,然后为其指定观察者。我注意到,这比其他方法要慢得多,例如做一个简单的循环并在每次迭代时通知观察者,或者使用Observable.Create方法而不是ToObservable。差异是巨大的:慢20到30倍。就是这样,还是我做错了什么?

using System;
using System.Diagnostics;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using System.Reactive.Threading.Tasks;

public static class Program
{
    static void Main(string[] args)
    {
        const int COUNT = 10_000_000;
        Method1(COUNT);
        Method2(COUNT);
        Method3(COUNT);
    }

    static void Method1(int count)
    {
        var source = Enumerable.Range(0, count);
        var subject = new Subject<int>();
        var stopwatch = Stopwatch.StartNew();
        source.ToObservable().Subscribe(subject);
        Console.WriteLine($"ToObservable: {stopwatch.ElapsedMilliseconds:#,0} msec");
    }

    static void Method2(int count)
    {
        var source = Enumerable.Range(0, count);
        var subject = new Subject<int>();
        var stopwatch = Stopwatch.StartNew();
        foreach (var item in source) subject.OnNext(item);
        subject.OnCompleted();
        Console.WriteLine($"Loop & Notify: {stopwatch.ElapsedMilliseconds:#,0} msec");
    }

    static void Method3(int count)
    {
        var source = Enumerable.Range(0, count);
        var subject = new Subject<int>();
        var stopwatch = Stopwatch.StartNew();
        Observable.Create<int>(o =>
        {
            foreach (var item in source) o.OnNext(item);
            o.OnCompleted();
            return Disposable.Empty;
        }).Subscribe(subject);
        Console.WriteLine($"Observable.Create: {stopwatch.ElapsedMilliseconds:#,0} msec");
    }
}

输出:

ToObservable: 7,576 msec
Loop & Notify: 273 msec
Observable.Create: 511 msec

.NET Core 3.0,C#8,System.Reactive 4.3.2,Windows 10,控制台应用程序,已发布版本


更新:以下是我要实现的实际功能的示例:

var source = Enumerable.Range(0, 10_000_000).Select(i => (long)i);
var subject = new Subject<long>();
var cntTask = subject.Count().ToTask();
var sumTask = subject.Sum().ToTask();
var avgTask = subject.Average().ToTask();
source.ToObservable().Subscribe(subject);
Console.WriteLine($"Count: {cntTask.Result:#,0}, Sum: {sumTask.Result:#,0}, Average: {avgTask.Result:#,0.0}");

输出:

数量:10,000,000,总和:49,999,995,000,000,平均值:4,999,999.5

与使用标准LINQ运算符相比,此方法的重要区别在于可枚举的源仅被枚举一次。


另一个观察结果::使用ToObservable(Scheduler.Immediate)ToObservable()快一点(大约20%)。

2 个答案:

答案 0 :(得分:6)

这是表现良好的可观察性与“因为您认为更快更好,但是不是”可观察性之间的区别。

当您深入探究消息源时,您会发现以下可爱的小句:

scheduler.Schedule(this, (IScheduler innerScheduler, _ @this) => @this.LoopRec(innerScheduler));

每次计划的递归迭代有效地调用hasNext = enumerator.MoveNext();

这使您可以为.ToObservable(schedulerOfYourChoice)呼叫选择调度程序。

通过选择的其他选项,您创建了对.OnNext的从头到尾的调用,几乎没有任何作用。 Method2甚至没有.Subscribe通话。

Method2Method1都使用当前线程运行,并且都在订阅完成之前运行完毕。他们正在阻止通话。它们可能导致比赛条件。

Method1是唯一表现良好的可观察对象。它是异步的,可以独立于订户运行。

请记住,可观察对象是随时间运行的集合。它们通常具有异步源或计时器,或对外部刺激的响应。他们通常不会遇到简单的枚举。如果您使用的是枚举,则应该期望同步运行速度更快。

速度不是Rx的目标。目标是对基于时间的推送值执行复杂的查询。

答案 1 :(得分:-1)

因为主体什么都不做。

在两种情况下,循环语句的性能似乎有所不同:

for(int i=0;i<1000000;i++)
    total++;

for(int i=0;i<1000000;i++)
    DoHeavyJob();

如果使用另一个Subject,且OnNext实施缓慢,则结果会更容易接受

using System;
using System.Diagnostics;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using System.Reactive.Threading.Tasks;

public static class Program
{
    static void Main(string[] args)
    {
        const int COUNT = 100;
        Method1(COUNT);
        Method2(COUNT);
        Method3(COUNT);
    }

    class My_Slow_Subject : SubjectBase<int>
    {

        public override void OnNext(int value)
        {
            //do a job which spend 3ms
            System.Threading.Thread.Sleep(3);
        }


        bool _disposed;
        public override bool IsDisposed => _disposed;
        public override void Dispose() => _disposed = true;
        public override void OnCompleted() { }
        public override void OnError(Exception error) { }
        public override bool HasObservers => false;
        public override IDisposable Subscribe(IObserver<int> observer) 
                => throw new NotImplementedException();
    }

    static SubjectBase<int> CreateSubject()
    {
        return new My_Slow_Subject();
    }

    static void Method1(int count)
    {
        var source = Enumerable.Range(0, count);
        var subject = CreateSubject();
        var stopwatch = Stopwatch.StartNew();
        source.ToObservable().Subscribe(subject);
        Console.WriteLine($"ToObservable: {stopwatch.ElapsedMilliseconds:#,0} msec");
    }

    static void Method2(int count)
    {
        var source = Enumerable.Range(0, count);
        var subject = CreateSubject();
        var stopwatch = Stopwatch.StartNew();
        foreach (var item in source) subject.OnNext(item);
        subject.OnCompleted();
        Console.WriteLine($"Loop & Notify: {stopwatch.ElapsedMilliseconds:#,0} msec");
    }

    static void Method3(int count)
    {
        var source = Enumerable.Range(0, count);
        var subject = CreateSubject();
        var stopwatch = Stopwatch.StartNew();
        Observable.Create<int>(o =>
        {
            foreach (var item in source) o.OnNext(item);
            o.OnCompleted();
            return Disposable.Empty;
        }).Subscribe(subject);
        Console.WriteLine($"Observable.Create: {stopwatch.ElapsedMilliseconds:#,0} msec");
    }
}

输出

ToObservable: 434 msec
Loop & Notify: 398 msec
Observable.Create: 394 msec

ToObservable支持System.Reactive.Concurrency.IScheduler

这意味着您可以实现自己的IScheduler并决定何时运行每个任务

希望这会有所帮助

致谢