I am trying to enumerate a large IEnumerable
once, and observe the enumeration with various operators attached (Count
, Sum
, Average
etc). The obvious way is to transform it to an IObservable
with the method ToObservable
, and then subscribe an observer to it. I noticed that this is much slower than other methods, like doing a simple loop and notifying the observer on each iteration, or using the Observable.Create
method instead of ToObservable
. The difference is substantial: it's 20-30 times slower. It is what it is, or am I doing something wrong?
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");
}
}
Output:
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, Console App, Release built
Update: Here is an example of the actual functionality I want to achieve:
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}");
Output:
Count: 10,000,000, Sum: 49,999,995,000,000, Average: 4,999,999.5
The important difference of this approach compared to using standard LINQ operators, is that the source enumerable is enumerated only once.
One more observation: using ToObservable(Scheduler.Immediate)
is slightly faster (about 20%) than ToObservable()
.
Stopwatch
is probably enough. – AmokMethod3(COUNT); Method2(COUNT); Method1(COUNT);
. I got similar results. – Amokvar source = Enumerable.Range(0, count);
) – WaineToObservable
is exactly 24.8 or 25.2 times slower. It doesn't make any difference for my use case. In both cases I am inclined not to use it, and use one of the other methods instead. – Amok.ToObservable()
for the reasons I outlined in my answer. Speed is not the goal here. – TrustyObservable.Create
and especially so if you end up doing areturn Disposable.Empty;
. – Trusty