Tại sao IEnumerable.ToObservable lại chậm như vậy?


9

Tôi cố gắng để liệt kê một lớn IEnumerablemột lần, và quan sát sự liệt kê với những hoạt động khác nhau kèm theo ( Count, Sum, Averagevv). Cách rõ ràng là chuyển đổi nó thành một IObservablephương thức ToObservable, và sau đó đăng ký một người quan sát vào nó. Tôi nhận thấy rằng điều này chậm hơn nhiều so với các phương thức khác, như thực hiện một vòng lặp đơn giản và thông báo cho người quan sát trên mỗi lần lặp hoặc sử dụng Observable.Createphương thức thay vì ToObservable. Sự khác biệt là đáng kể: nó chậm hơn 20-30 lần. Đó là những gì nó là, hoặc tôi đang làm gì đó sai?

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");
    }
}

Đầu ra:

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, Ứng dụng bảng điều khiển, Bản phát hành được xây dựng


Cập nhật: Đây là một ví dụ về chức năng thực tế tôi muốn đạt được:

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}");

Đầu ra:

Đếm: 10.000.000, Tổng: 49.999.995.000.000, Trung bình: 4.999.999,5

Sự khác biệt quan trọng của phương pháp này so với việc sử dụng các toán tử LINQ tiêu chuẩn , là liệt kê nguồn chỉ được liệt kê một lần.


Thêm một quan sát: sử dụng ToObservable(Scheduler.Immediate)nhanh hơn một chút (khoảng 20%) so với ToObservable().


2
Một phép đo 1 lần không quá đáng tin cậy. Ví dụ, xem xét việc thiết lập một điểm chuẩn với BenchmarkDotNet . (Không liên kết)
Fildor

1
@TheodorZoulias Có nhiều điều hơn thế, ví dụ, tôi sẽ đặt câu hỏi về điểm chuẩn của bạn vì nó hiện đang tồn tại vì thứ tự thực hiện trong lần chạy đó có thể gây ra sự khác biệt lớn.
Oliver

1
Đồng hồ bấm giờ có thể là đủ, nếu bạn thu thập số liệu thống kê. Không chỉ là một mẫu duy nhất.
Fildor

2
@Fildor - Đủ công bằng. Tôi có nghĩa là những con số là đại diện cho những gì người ta nên mong đợi.
Enigmativity

2
@TheodorZoulias - Câu hỏi hay, btw.
Enigmativity

Câu trả lời:


6

Đây là sự khác biệt giữa một hành vi có thể quan sát tốt và "quan sát chính bạn vì bạn nghĩ nhanh hơn là tốt hơn nhưng không thể" quan sát được.

Khi bạn lặn xuống đủ xa trong nguồn, bạn phát hiện ra dòng nhỏ đáng yêu này:

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

Đây là cách gọi hiệu quả hasNext = enumerator.MoveNext();một lần cho mỗi lần lặp đệ quy theo lịch trình.

Điều này cho phép bạn chọn lịch trình cho .ToObservable(schedulerOfYourChoice)cuộc gọi của bạn .

Với các tùy chọn khác mà bạn đã chọn, bạn đã tạo ra một loạt các cuộc gọi trực tiếp .OnNextmà hầu như không làm gì cả. Method2thậm chí không có một .Subscribecuộc gọi.

Cả hai Method2Method1chạy bằng cách sử dụng luồng hiện tại và cả hai chạy đến hoàn thành trước khi đăng ký kết thúc. Họ đang chặn cuộc gọi. Họ có thể gây ra điều kiện chủng tộc.

Method1là người duy nhất cư xử độc đáo như một người có thể quan sát được. Nó không đồng bộ và nó có thể chạy độc lập với thuê bao.

Hãy nhớ rằng các đài quan sát là các bộ sưu tập chạy theo thời gian. Họ thường có nguồn không đồng bộ hoặc bộ đếm thời gian hoặc đáp ứng với kích thích bên ngoài. Họ không thường chạy khỏi vô số. Nếu bạn đang làm việc với vô số thì nên làm việc đồng bộ để chạy nhanh hơn.

Tốc độ không phải là mục tiêu của Rx. Thực hiện các truy vấn phức tạp trên các giá trị dựa trên thời gian, đẩy là mục tiêu.


2
"Roll-your-own-own-you-think-quick-is-better-but-it-is-not" - tuyệt vời !!
Fildor

Cảm ơn Enigmativity cho câu trả lời chi tiết! Tôi đã cập nhật câu hỏi của mình với một ví dụ về những gì tôi thực sự muốn đạt được, đó là một tính toán đồng bộ về bản chất. Bạn có nghĩ rằng thay vì các phần mở rộng Reactive tôi nên tìm kiếm một công cụ khác, cho rằng hiệu suất là rất quan trọng trong trường hợp của tôi?
Theodor Zoulias

@TheodorZoulias - Đây là cách đếm để làm ví dụ của bạn trong câu hỏi của bạn: 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 }). Một lần lặp duy nhất và nhanh hơn 10 lần so với Rx.
Enigmativity

Tôi mới thử nó, và nó thực sự nhanh hơn, nhưng chỉ nhanh hơn khoảng x2 (so với RX không có ToObservable). Đây là một thái cực khác, nơi tôi có hiệu suất tốt nhất nhưng tôi buộc phải thực hiện lại mọi toán tử LINQ bên trong một biểu thức lambda phức tạp. Đó là lỗi dễ xảy ra và ít bảo trì hơn, vì các tính toán thực tế của tôi liên quan đến nhiều toán tử hơn và các kết hợp của chúng. Tôi nghĩ rằng việc trả giá hiệu suất x2 là khá hấp dẫn để có giải pháp rõ ràng và dễ đọc. Mặt khác trả x10 hoặc x20, không quá nhiều!
Theodor Zoulias

Có lẽ nếu bạn đăng chính xác những gì bạn đang cố gắng tôi có thể đề xuất một giải pháp thay thế?
Enigmativity

-1

Vì môn không làm gì.

Có vẻ như tính chính xác của câu lệnh lặp là khác nhau đối với 2 trường hợp:

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

hoặc là

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

Nếu sử dụng Chủ đề khác, với việc triển khai OnNext chậm, kết quả sẽ được chấp nhận hơn

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");
    }
}

Đầu ra

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

Hệ thống hỗ trợ ToObservable.Reactive.Concurrency.IScheduler

Điều đó có nghĩa là bạn có thể thực hiện IScheduler của riêng mình và quyết định khi nào chạy từng tác vụ

Hi vọng điêu nay co ich

Trân trọng


Bạn có nhận ra OP đang nói rõ ràng về giá trị COUNT cường độ cao hơn 100.000 lần không?
Fildor

Cảm ơn BlazorPlus cho câu trả lời. Tôi đã cập nhật câu hỏi của mình thêm một ví dụ thực tế hơn về trường hợp sử dụng của tôi. Điều subjectnày được quan sát bởi các toán tử khác thực hiện tính toán, vì vậy nó không làm gì cả. Hình phạt hiệu suất của việc sử dụng ToObservablevẫn còn đáng kể, bởi vì các tính toán rất nhẹ.
Theodor Zoulias
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.