IEnumerable.ToObservableが非常に遅いのはなぜですか?


9

私は大規模な列挙しようとしていますIEnumerable一度、および添付のさまざまな演算子(と列挙を観察しCountSumAverageなど)。明白な方法は、それをIObservableメソッドToObservableでに変換し、オブザーバーをサブスクライブすることです。これは、単純なループを実行して各反復でオブザーバーに通知する、または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演算子を使用する場合と比較したこのアプローチの重要な違いは、列挙可能なソースが1回だけ列挙されることです。


もう1つの観察:を使用する方ToObservable(Scheduler.Immediate)がよりもわずかに高速(約20%)ですToObservable()


2
1回の測定だけでは信頼性が高くありません。たとえば、BenchmarkDotNetを使用してベンチマークを設定することを検討してください。(系列外)
Fildor

1
@TheodorZouliasそれだけではありません。たとえば、現在、1回の実行内での実行の順序が大きな違いを引き起こしている可能性があるため、ベンチマークに疑問を投げかけます。
オリバー

1
統計を集めればストップウォッチで十分かもしれません。単一のサンプルだけではありません。
Fildor

2
@Fildor-十分に公正です。つまり、これらの数値は予想されるものの代表的なものです。
謎解き

2
@TheodorZoulias-いい質問ですね、ところで。
謎解き

回答:


6

これは、適切に動作するオブザーバブルと「自分でロールするために考える」という観察可能なものとの違いです。

ソースを十分に掘り下げると、この素敵な小さなラインが見つかります。

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

は、hasNext = enumerator.MoveNext();スケジュールされた再帰的な反復ごとに1回、効果的に呼び出します。

これにより、スケジューラを選択できます .ToObservable(schedulerOfYourChoice)コール。

選択した他のオプションを使用し.OnNextて、実質的に何も行わない一連の呼び出しを作成しました。Method2さえ持っていません.Subscribe呼び出し。

両方Method2Method1サブスクリプションが終了する前に完了するまで、現在のスレッドとの両方の実行を使用して実行します。彼らは通話をブロックしています。競合状態を引き起こす可能性があります。

Method1オブザーバブルとしてうまく動作する唯一のものです。これは非同期であり、サブスクライバーとは関係なく実行できます。

オブザーバブルは時間をかけて実行されるコレクションであることに注意してください。彼らは通常、非同期ソースまたはタイマーまたは外部刺激への応答を持っています。彼らはしばしば単純な列挙型を使い果たすことはありません。列挙型で作業している場合は、同期して作業するほうが高速に実行されることが期待されます。

速度はRxの目標ではありません。時間ベースのプッシュされた値に対して複雑なクエリを実行することが目標です。


2
「自分でロールするため、自分で考えた方が速い-優れているが、そうではない」-すばらしい!!
Fildor

詳細な回答を提供してくれたEnigmativityに感謝します。私は実際に達成したいことの例である質問を更新しました。これは本質的に同期的な計算です。私の場合パフォーマンスが重要であることを考えると、Reactive拡張機能の代わりに別のツールを検索する必要があると思いますか?
Theodor Zoulias

@TheodorZoulias-あなたの質問であなたの例を行うための列挙可能な方法は次のとおりsource.Aggregate(new { count = 0, sum = 0L }, (a, x) => new { count = a.count + 1, sum = a.sum + x }, a => new { a.count, a.sum, average = (double)a.sum / a.count })です:。1回の反復のみで、Rxよりも10倍以上高速です。
謎解き

私はそれをテストしたところ、確かに高速ですが、x2ほど高速です(なしのRXと比較してToObservable)。これは、私が最高のパフォーマンスを発揮するもう1つの極端な方法ですが、複雑なラムダ式内にすべてのLINQ演算子を再実装することを余儀なくされます。実際の計算にはさらに多くの演算子とそれらの組み合わせが含まれることを考えると、エラーが発生しやすく、保守性が低下します。明確で読みやすいソリューションを提供するためにx2のパフォーマンス価格を支払うのは非常に魅力的だと思います。一方、x10またはx20を支払うと、それほど多くありません。
Theodor Zoulias

おそらく、あなたがやろうとしていることを正確に投稿した場合、代替案を提案できますか?
謎解き

-1

被験者は何もしないからです。

ループステートメントのパフォーマンスは2つのケースで異なるようです。

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

または

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

OnNextの実装が遅い別のSubjectを使用すると、結果はより受け入れやすくなります

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を実装して、各タスクをいつ実行するかを決定できます。

お役に立てれば

よろしく


OPが100万倍の大きさのCOUNT値について明示的に話していることに気づいていますか?
Fildor

答えてくれてBlazorPlusに感謝します。私の使用事例のより現実的な例を追加して質問を更新しました。subject計算を実行し、他の事業者によって観察されるので、何もしないではないです。ToObservable計算が非常に軽いため、使用によるパフォーマンスのペナルティは依然としてかなりのものです。
Theodor Zoulias
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.