-
Notifications
You must be signed in to change notification settings - Fork 751
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Observable completion exhibits O(n^2) behavior in GroupBy+[SelectMany/Merge] in the number of groups #2005
Comments
Thanks for all the details! |
I think there are two possible strategies we could use to fix this:
I think 1 might be tricky in this case, because it's the Also, attempting 1 seems less general: it seems like it would be too easy to end up solving this only for certain specific combinations of known operators, and it would definitely break if someone threw their own operator into the mix. Also, there are cases where you could have thousands of groups, and they would not necessarily all complete at once. Ideally we'd want to make that more efficient, instead of fixing only the very particular case in which everything shuts down at once. So approach 2 seems more promising. The way I would approach this would, I think, be to make So I'm thinking we'd change the field that currently is of type The alternate representation couldn't be a In cases where the |
I've prototyped approach 2 described above, making See #2092 Here are how your benchmarks look on my machine (which is quite old now, and significantly slower than yours) running against the current (The dark blue line here lines up almost precisely with the green one here which is why it appears to be missing. Likewise the orange line behind the pale blue one.) You can see the lines curving upwards in way that looks consistent with the Here's how it looks with prototype: Note that the vertical scale is different—this goes up only to 600 whereas the first went up to 3500. (Not quite sure why it went for that instead of 3000, but there we are.) Because we're effectively zoomed in a bit more, it's now possible to see that there really are 4 lines, thanks to minor variations in the runs. For comparison, if I show it at the same vertical scale as the first graph, you can see that it's a significant overall improvement for larger group sizes: Perhaps more importantly, we no longer see the upward curve. It looks consistent with the predicted My one concern with this was that because Each point represents one particular ( The red line marks X=Y. Any points falling on this line ran at the same speed before and after the change. Any falling above it got slower, and any falling beneath it got faster. You can see the positive effect of this change very easily—the points that are way off the line and below it all represent the new benchmarks added to capture this problem, and their position on this plot shows that they all got significantly faster. We can also see that nothing got dramatically slower. There's a bit of variation, but I think that's just variability between runs. (I was running this suite on my desktop machine and since it takes several hours, I was doing other things at the same time.) We are aiming to introduce some more consistent benchmark analysis, with execution procedures that should produce less variable results, and we also want to run the benchmarks over some older versions of Rx to see if any performance regressions were introduced. So at this stage, with respect to this particular problem, our main concern is to determine that we didn't cause any major new problems. It's looking OK from that perspective. |
Note, this change went into |
System.Reactive 6.0.0
dotnet 7.0.203
We are processing an observable that contains sensor samples taken from many different sensors. We want to gather statistics related to each individual sensor, and so perform a
GroupBy()
on the observable to create per-sensor observables. There is a long delay between the observable completing and the final subscriber completing during which there is 100% CPU usage.A simple reproducer has been created based on an observable:
IObservable<IGroupedObservable<...>>
in n groupsMerge()
orSelectMany()
.For example:
or
The time taken to complete is O(m) where m is the number of elements in the array.
The time taken to complete is O(n^2) where n is the number of groups that were allocated (
numberOfGroups
from the above example).Running under a profiler the problem appears to be that when each of the
IGroupedObservables
completes, the subscriber created by theSelectMany()
orMerge()
is individually removed from aCompositeDisposable
. This removal results in a linear search of anIList
in theCompositeDisposable
that contains one entry per group. As all the subscribers are removed, one after the other, this removal process is O(n^2) on the number of groups.Profiler output:
The following class runs under BenchmarkDotNet to exhibit the issue:
Sample output is:
Note that increasing the number of samples has a relatively consistent increase the total time of about 100 ms, whereas increasing the number of groups has a more quadratic relationship with overall time, becoming the dominating factor at around 100,000 groups.
The text was updated successfully, but these errors were encountered: