-
Notifications
You must be signed in to change notification settings - Fork 7.6k
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
Scheduled action no interrupt #1898
Scheduled action no interrupt #1898
Conversation
Some perf (* this PR)
I've rerun observeOn * 1000 a couple of times and I always get such low values which I don't understand; could be the effect of the increased memory usage tripping over some cache sizes. |
Somehow, observeOn x 1000 runs slower no matter what optimizations I try, but for the rest, the changes are generally improving the throughput. |
@@ -66,22 +68,31 @@ public boolean isUnsubscribed() { | |||
|
|||
@Override | |||
public void unsubscribe() { | |||
if (ONCE_UPDATER.compareAndSet(this, 0, 1)) { | |||
if (!cancel.isUnsubscribed()) { |
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 isn't Atomic like the ONCE_UPDATER was, so are you just trusting the cancel
Subscription to be idempotent in the event of a race?
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. I ran some profiling and saw cancel.unsubscribe() to be expensive. Adding this check reduced the time spent in unsubscribe() by half. Usually, there isn't any race but just the self cancellation and then when the ScheduledAction is removed from its parent's CompositeSubscription.
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 understand. The ONCE_UPDATER would be sure we only call unsubscribe
once, and it needs to be called at least once, so what performance benefit happened by removing ONCE_UPDATER that now allows it to possibly be called more than once?
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 is known the ScheduledAction.unsubscribe is called at least twice: once at the end of the run and a second time because the task is removed via CompositeSubscription.remove() from the parent scheduler. If the task terminates normally, the second call to unsubscribe will guaranteed to fail the CAS which has high cost anyways. Testing the cancel.isUnsubscribed() is effectively a volatile read with far less cost. In addition, it saved 4 bytes of memory per instance now usable by passing the thread around.
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.
What is the cost of a failed CAS compared with a volatile read?
I'm not arguing that testing isUnsubscribed() is cheap or expensive, just that it's not atomic. We could end up calling unsubscribe twice without the CAS check.
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.
On X86, volatile read cost the same as any regular memory read. CAS is around 20-40 cycles. I've run a small program and measured always failing and always succeding CAS on my i7 4770 @ 3.5GHz and gave me ~5.2 ns in both cases, If I test the variable before CAS, I get 1.6ns for always fails and 8.8ns for always succeeding. So for 2 unsubscribe calls, we get 10.4ns in the CAS only case and 10.4ns with read-before-cas. However, any additional unsubscribe calls adds only 1.6ns with read-before-cas instead of 5.2ns. I must admit, the 2 unsubscribe calls costing the same time suprises me.
The unsubscribe() is still idempotent because either cancel.isUnsubscribed returns true and nothing happens or cancel.unsubscribe() is called which is idempotent.
public class CasFailureTest {
public static void main(String[] args) {
AtomicInteger v = new AtomicInteger(0);
// AtomicInteger v = new AtomicInteger(1);
int k = 100_000_000;
for (int i = 0; i < 10; i++) {
long n = System.nanoTime();
for (int j = 0; j < k; j++) {
if (v.get() == j) {
v.compareAndSet(j, j + 1);
}
// if (v.get() == 0)
// v.compareAndSet(0, 1);
}
System.out.println(1d * (System.nanoTime() - n) / k);
v.set(0);
}
}
}
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 is great information! Do you think it would be valuable to convert this into a JMH test and commit it with the baseline benchmarks similar to https://github.com/ReactiveX/RxJava/blob/1.x/src/perf/java/rx/PerfBaseline.java along with a README of what costs are for these different things?
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.
or cancel.unsubscribe() is called which is idempotent.
This is the part that I wasn't sure of, so if that is idempotent then there definitely is not a need for the extra ONCE_UPDATER. Thanks for walking me through this patiently.
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.
As an exercise to learn JMH, sure. Otherwise, it would be a curiosity only.
Merging as these changes look good and I understand them now well enough to feel confident with them. The discussion about whether we should be interrupting by default is somewhat orthogonal to this PR. This makes the current behavior work more safely. Thank you @akarnokd |
Scheduled action no interrupt
See #1911 for the JMH benchmarks. |
@benjchristensen @akarnokd with 1.0.2 my interrupts are gone! |
At the end of the run, ScheduledAction unsubscribes itself which triggers a set of cleanup actions, and since it is also the way to cancel a running action, it calls future.cancel(true) as well which interrupts the task in the underlying executor framework. However, on a normal completion path, it is unnecessary to interrupt itself at the end of the run since it will complete anyway, and not to mention, it has some performance implications for short running tasks. The downside is the +4 bytes per ScheduledAction.