ReactiveX / RxCpp

Reactive Extensions for C++
Apache License 2.0
3.05k stars 395 forks source link

How does the light-weight tail recursion work? #489

Open iam opened 5 years ago

iam commented 5 years ago

There's a lot of classes in rx-scheduler.hpp like recursed_scope_type, recursed_scope, recursed, recursion, etc.

Is there some general principle tying these together?

Calling what(state->r.get_recurse()); seems to be the general pattern to invoke these, but even after reading the code in rx-scheduler.hpp it's hard to understand what the line of code is actually supposed to do?


Based off history like https://github.com/ReactiveX/RxCpp/issues/2 I am guessing the intent was to eliminate virtual calls.

However it's not obvious how it's doing this, and why simply exhausting the entire queue in a single loop wasn't done instead?

   while (!q.empty()) { 
     auto what = q.pop();
     what();
   }
kirkshoop commented 5 years ago

Yes, the recurse.. types are used to avoid virtual calls.

This is primarily for generators. the range is an example of recursively scheduling the same function to produce each value in the range. This was used to increase the performance of a range.

RxJava chose a different path where generators like range are just a for loop calling next().

I now think that the recurse.. and coordinate.. patterns turned out to add more complexity than value.

iam commented 5 years ago

This is primarily for generators. the range is an example of recursively scheduling the same function to produce each value in the range. This was used to increase the performance of a range.

I am curious now, would you mind elaborating :smiley: ? Using manual recursion is faster than a for-loop?

kirkshoop commented 5 years ago

Using manual recursion is faster than a for-loop?

😆 nope, your intuition is correct. this was about a tradeoff.

$ ./rxcpp_test_subject [perf]
..
loop -> subscriber  : 1 subscribed, 10000000 on_next calls, 15ms elapsed 6.66667e+08 ops/sec 
range -> subscriber : 1 subscribed, 10000000 on_next calls, 77ms elapsed 1.2987e+08 ops/sec 
..

The code for the loop is:

auto o = rx::make_subscriber<int>(
  [](int){++c;},
  [](rxu::error_ptr){abort();});
    for (int i = 0; i < onnextcalls && o.is_subscribed(); i++) {
      o.on_next(i);
    }
    o.on_completed();

The code for the range is:

rxs::range<int>(1, onnextcalls).subscribe(
  [](int){
    ++c;
  },
  [](rxu::error_ptr){abort();});