-
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
Add dedicated class based implementations for Append and Prepend. #600
Add dedicated class based implementations for Append and Prepend. #600
Conversation
|
||
protected override void Run(_ sink) => sink.Run(_source); | ||
|
||
internal sealed class _ : IdentitySink<TSource> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be great if we abandoned these underscore class names.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with you, but that name is used in most - if not all - operators I have seen.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Definitely desirable, maybe in a later PR.
private static IDisposable ForwardValue(IScheduler scheduler, _ sink) | ||
{ | ||
sink.ForwardOnNext(sink._value); | ||
return scheduler.Schedule(sink, ForwardOnCompleted); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it necessary to schedule the OnCompleted
signal separately?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, that was something I tried to ask in #567. The conclusion was to do it like ToObservable
/StartWith
do it. And ToObservable
does schedule the OnCompleted
separately.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we need the separate schedule here.
private static IDisposable ForwardOnCompleted(IScheduler _unused, _ sink) | ||
{ | ||
sink.ForwardOnCompleted(); | ||
return BooleanDisposable.True; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should not really be returned by operators and is supposed to be confined to Disposable
methods. Return Disposable.Empty
instead.
protected override void Dispose(bool val) | ||
{ | ||
base.Dispose(val); | ||
Disposable.TryDispose(ref _schedulerDisposable); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Usually we do this as if (disposing) { dispose custom resources } base.Dispose(disposing)
.
One thing to consider is that IObservable<int> someSource = Observable.Return(0);
for (int i = 1; i < 1000; i++) {
someSource = someSource.Append(i);
}
someSource.Subscribe(Console.WriteLine); Does this still work with your modifications? |
It is tested for three consequtive |
|
||
internal sealed class _ : IdentitySink<TSource> | ||
{ | ||
private readonly IObservable<TSource> _source; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This field is unused.
|
We could go the same route, as the corefx team has done it for the enumerable counter part [1], to optimize consecutive
If we do that or anything else in that direction, we should propably skip the extra scheduling of the I can do that, but that'd take some time. |
It is certainly more interesting and challenging doing this type of "operator fusion". I leave it up to you if you want to tackle it. |
I'd give it a try than, if that's ok for @danielcweber as well. |
Sure, go ahead. |
Looks good. Is there still something missing to make the WIP go away? |
Not really. I first thought I put the optimizations, I'm working on, into this PR. But I think it is a good point to merge. I'll put the next commits in a new PR. |
@akarnokd now the stacktrace should be much smaller, hopefully. 😄 |
@danielcweber, I changed my mind and put the changes into this PR. Let me know if I can help you during the review. |
Unfortunately, I get 4 failing tests with NullReferenceExceptions locally:
|
} | ||
} | ||
|
||
private sealed class State |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please replace this class by the use of C#7 ValueTuples to have less allocations.
Sorry, I was pretty sure that I did had run all tests the day before. I'll fix that. |
Man, in that commit was nearly everything wrong that could go wrong, sorry for that. I hope it's now ok. At least all tests pass. I changed the class to be a struct like you did in #644. |
if (_prepends != null) | ||
{ | ||
var disposable = Schedule(_prepends, s => s.SetUpstream(s._source.SubscribeSafe(s))); | ||
Disposable.TrySetSerial(ref _schedulerDisposable, disposable); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That could indeed lead to trouble, in case the subscription (one line above) will complete synchronously, you will effetively be overwriting the scheduling from OnCompletd, thus cancelling that work. TrySetSingle should be fine.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I changed it to TrySetSingle
. Should I than use two different backing fields for the two schedulers? Currently, they share one field variable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, the one field should be fine, it's just the TrySetSingle
should only be on the first scheduling. Now you're using it on both, which is not what I meant. In fact, if you change both to SetSingle
instead of TrySetSingle
, you will probably notice one of the tests throwing. The second TrySetSingle
will fail in the case that we have prepends and appends. So we're currently leaking the second scheduling.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The second time you schedule something, you want it to overwrite the previous scheduling, which is why you want to use TrySetSerial
. The first time you schedule, however, you want to either set the disposable for the very first time or not set it at all. Because, if the scheduled work executes synchronously, the second scheduling will take place (successfully through TrySetSerial), and by the time Schedule returns, we definitely don't want to overwrite that second scheduling, which is why we want to use TrySetSingle.
_scheduler = parent.Scheduler; | ||
|
||
if (parent._prepends != null) | ||
_prepends = parent._prepends.ToArray(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we avoid the creation of this array by simply walking the node structure?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, we could. I haven't done it because we then have to duplicate the two loops. It's a tradeoff between code reuse and one allocation.
_prepends = parent._prepends.ToArray(); | ||
|
||
if (parent._appends != null) | ||
_appends = parent._appends.ToReverseArray(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This array creation could be avoided as well if the appends where in a doubly-linked node chain. In that case, it would be enough to traverse the chain once to find the very first value and then walking backwards.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Beware, by doubly linking the nodes, we can't pass around the original observable once we appended/prepended to it, or even append/prepend something to the original observable. The instances we get from appying operators should be, in a way, immutable. We would get subtle side effects here, if I got your idea correctly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was thinking about getting the immutable collections back in (#222), then it's just a stack and a queue.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The instances we get from appying operators should be, in a way, immutable.
Indeed, the _appends
should be an immutable list. I was somehow thinking it being a copy-on-write list (i.e., ImmutableList) which requires way more allocation and copying. My mistake.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we manage to mitigate the performance degradation that has been reported in #222, immutable collections could be helpful in a lot of places. It's used internally in the Roslyn compiler since it's highly concurrent.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The current implementation has a time complexity of O(n) and needs n + 1 allocations. I doubt the immutable collections offer a datatype that can beat that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adding a node to the lists as they are currently implemented is even in O(1). Yes, even immutable collections won't beat that. Maybe they'll find their way into the project eventually since they would be useful in the Subjects, but until that, this solution is perfectly fine.
Here is my attempt to optimize the
Append
andPrepend
operators. Please review it carefully, I'm not sure if I got the handling of theIDisposables
right.