-
Notifications
You must be signed in to change notification settings - Fork 402
[Perfomance] Significantly reduce amount of copies/moves inside operators #562
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
Conversation
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.
Incredible work. Very consistent application of the copy vs. allocation tradeoff. I am not sure that this tradeoff will work for every user, but I would like to try it out.
The tests are wonderful - they will make this tradeoff selection much easier to maintain.
one small typo is in all the tests
GIVEN("observale and subscriber")
observable is missing the second b. Trivial and does not block the PR.
I will try this out on one of my more complicated programs and see if it has any issues.
Thank you for your time for reviewing and comments. Waiting for any future comments or merge =) P.S. I have plans to do something similar for observable part. But I believe, it will be a bit more complicated |
I tried this out and I cannot compile my project. https://github.com/kirkshoop/twitter
The issue seems to be forcing const& value passing. Forwarding would be fine, but const breaks this pattern: https://github.com/kirkshoop/twitter/blob/master/main.cpp#L1089..L1118 scan owns the Model instance and ViewModel is expected to mutate the instance in scan. |
Is it expected behavior? I mean, from the point of "functional programming" view it is unexpected to change "original" variable in map, isn't it? auto observable = rxcpp::observable<>::just(1).publish();
// subscriber 1
observable.map([](int& v) {v += 2; return v;}).subscribe();
// subscriber 2
observable.subscribe();
observable.connect(); then subscriber 2 obtains incorrect/unexpected value, isn't it? Also, your example contains this lines: start_with(Model{}) |
rxo::map([](Model& m){
return ViewModel{m};
}) | In case of "start_with" ViewModel contains reference to object inside start_with. Depending on implementation it can has as reference to temporal object (if start_with do copy for each new subscriber) or modify original value inside start_with (if no copy). Not sure if it is expected |
It is arguable. reduce and scan explicitly mutate the accumulator and scan intentionally emits the accumulator each time it changes. should subsequent operators be allowed to mutate the accumulator? It is safe for scan. the next value will not be accumulated until the stack unwinds from the on_next() that reports the current accumulator. In practice, I had already made the tradeoff such that Model has a single member that is a shared_ptr. This made it fairly easy to get it to compile with your changes. However, this is a breaking change. I think that I will create a new branch to hold the current state of rxcpp before I apply this PR. This will leave a path for others that are dependent on the previous design. |
The goal would be to allow just to control mutability. it either can always pass const& to on_next, or take a copy of value before calling on_next or delegate to the caller. Delegation would support passing in a The best world (from a C++ perspective) would keep the types directly under the users control - so if the user wanted a value to move or forward or copy or mutable&, through an expression the user would be able to specify the proper type decorations and the operators would preserve those decorations. That is a lot more work. It was the intent of the original design, but it got lost and ignored too many times and devolved to the inconsistent treatments for values that exist today. If you wanted to take on that task, I would be pleased. I have expectation on you and it will not block this PR. Thank you! |
I would try to think about it after this PR |
@kirkshoop, i have some ideas about providing ability to use mutable values, but i need some clarifications from you: Suggested there solution has ability to pass real const reference (or rvalue reference) on original object till final observer or any operator (if no any operator with caching mechanism). As a result, each operator operates at each time const lvalue reference or rvalue reference. From this point of view, what does mean "non-const" reference? I mean, non-const reference from const reference -> impossible. non-const reference from rvalue reference - okay, possible, but does it make sense? Looks like you have next options:
So, final question is: Does it correct, that you want: if original value was some "non-convertible-to-reference" type (const T & or const T), then we do copy for passing it to reference argument. If it is true, then ok, i can try to implement it |
If it is applicable, we can do something like this: TypeDeducer - is special wrapper to resolve all situations. It will be argument in call_lambda - imitation of operator (for example, map) where first param - lambda, second - gotten value. Template parameter is equal to argument of lambda Check output of execution to understand if it is expected behavior. In some combintations we do extra copy/move, but anyway it is better than conditionalless copies as before UPD: Updated version with reducing moves/copies https://godbolt.org/z/6bx1azK4a (std::optional can be chaned to maybe) |
I am not sure what you mean.. rx-map.hpp
map() makes no copies of |
I would expect a compile failure if a source value type was I don't like magic :) Conversions should be explicit - either in the definition of the operator (via has to take copies of the value) or in the users code (a lambda taking
Thus my answer is that I would like this to fail at compile time. the way to choose the type of the Source Value is to set the type of the Source Value.
The lambda arg type is not going to impact the implementation of any operator. If there is no conversion from the Source Value, then there will be a compile error. |
While that code is cool, it is not the behaviour I would prefer. It is more magical than I like :) |
Yes, but template<class U>
void operator()(U u) {
.....
that->destination.on_error(std::move(ex));
....
} |
This is what I see in nextdetacher
I do not see a copy.. |
With merged changes -> yes =) i told about rxcpp without this merge |
Ah! Well thank you! That is much better 😳 |
I've tried to implement idea with reference and your requirements above about compilation errors: looks like compilator against this idea and wants to error everytime. Problem is: |
Would you push the new branch as it is so that I can take a look? Perhaps we can move this work into a new issue, do you mind creating an issue for this? |
Significantly reduce amount of copies/moves inside operators. Each operator checked and fixed.. In most cases via forwarding/universal reference, in some cases via const reference (if operator is read-only). Each fix covered by unit test