Skip to content
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

Use perfect forward #5388

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open

Use perfect forward #5388

wants to merge 4 commits into from

Conversation

cyyever
Copy link
Contributor

@cyyever cyyever commented Sep 29, 2024

Description

Change some templates to use perfect forward. The forwarded items mostly argument annotations and other pybind11 objects so there should be benefits of moving them.

Copy link
Collaborator

@rwgk rwgk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me.

@Skylion007 for a 2nd set of eyes.

@cyyever how did you pin-point these changes? — I'd guess there are other similar situations in other files, but maybe you looked around already? Could you please explain in the PR description?

@wjakob
Copy link
Member

wjakob commented Sep 29, 2024

It's a good default policy to use perfect forwarding. But in this case, it's not clear to me what the actual benefit is.

@rwgk
Copy link
Collaborator

rwgk commented Sep 29, 2024

In the meantime it crossed my mind: This PR doesn't add tests or modify any unit tests. That's not ideal.

My general rule of thumb: If there is no test addition or change, it's probably not worth spending time on it (whatever "it" may be).

To turn this around: Best practice is to write or (rarely) change a test first, then fix.

@cyyever
Copy link
Contributor Author

cyyever commented Sep 30, 2024

@rwgk Grepping the source files to find out parameter packs.
With respect to tests, there is already test coverage for cpp_function and other changes. However, before the PR the arguments were passed as const references, now that they are passed differently.

@cyyever cyyever requested a review from rwgk September 30, 2024 03:21
@rwgk
Copy link
Collaborator

rwgk commented Oct 8, 2024

@rwgk Grepping the source files to find out parameter packs. With respect to tests, there is already test coverage for cpp_function and other changes. However, before the PR the arguments were passed as const references, now that they are passed differently.

Of course.

What's missing in this PR: a test that only works with your changes.

Now, this PR clearly is a good change.

But it's also entirely inconsequential, and there is nothing that prevents the (theoretical) improvement from accidentally getting lost again in some refactoring work.

So this PR creates a disturbance (and a distraction for maintainers) without any tangible benefit.

General guideline:

If you believe something is important:

Always write a test FIRST that fails if the production code changes are reverted.

If you come to the conclusion it's not worth the time writing the tests: Drop the idea entirely.

There are exceptions to this, like fixing a bug for a situation that is extremely difficult to capture in a unit test. But this PR isn't in that category.

@dalboris
Copy link
Contributor

dalboris commented Oct 9, 2024

Just randomly came across this, so I might as well provide my opinion.

Switching a function argument from a const ref (const T&) to a universal ref (templated T&&) makes two API changes:

  1. The function now also accepts non-const references (e.g., int&)
  2. The function now also accepts rvalue references (e.g., int&&)

The first point may or may not be desirable. Could it hide bugs further down the call chain, where now the function calls an overload that mutates the passed reference? Example:

void foo(const int& t) {  }
void foo(int& t) {  t = 42; }

template<typename T>
void bar(T&& t) {
    foo(std::forward<T>(t));
}

template<typename T>
void baz(const T& t) {
    foo(t);
}

int x = 1;
bar(x); // mutates x
baz(x); // does not mutate x

The second point is only useful for performance, but only if the argument is actually moved. Does the function creates resources from the arguments that outlive the function call, that can take advantage of stealing the resources given as arguments? If yes, then there can be a performance gain. Otherwise, if the function only uses its arguments, but never needs to copy it, then there is no performance gain to be had. I do not feel like knowing the internal of pybind11 enough to know in which case we are, but since Extra is so generic, I guess there might be cases where there are indeed copies that are done, that may benefit from moving instead.

@cyyever
Copy link
Contributor Author

cyyever commented Oct 9, 2024

@dalboris I understand your concerns. However, given the examples, usage of universal ref actually reveals the underlying inconsistency in API design, in that two versions of the foo function try to do semantically different things with the argument. This is not the fault of universal ref, function overloading should be blamed instead.
Nevertheless, in my opinion, universal ref was introduced in C++11 solely for the possible performance gains, with the extra complexity of reference resolution (from the perspective of the compiler and the developers).
It's reasonable to add news tests according to the suggestions of @rwgk , which I will do ASAP. Then it's up to the community to decide whether to use universal ref.

@dalboris
Copy link
Contributor

dalboris commented Oct 9, 2024

Nevertheless, in my opinion, universal ref was introduced in C++11 solely for the possible performance gains, with the extra complexity of reference resolution (from the perspective of the compiler and the developers).

No, rvalue references were introduced for performance gains. The different concept of universal references was introduced to write generic code that accepts either const ref, mutable ref, or rvalue ref, without the boilerplate of having to define three overloads for this.

Your commit has the goal to increase performance by allowing the functions to accept rvalue references. You do this by using universal references arguments. But this has the side effect that now, the functions also accept mutable references, which may or may not be desirable. I'm not saying whether or not it is desirable here. I'm just saying that it's one important thing to consider in the discussion.

@Skylion007
Copy link
Collaborator

@rwgk If we want unit tests, we just need to update clang-tidy and the static analysis in the latest versions will flag it. I think that's how @cyyever found these issues.

@rwgk
Copy link
Collaborator

rwgk commented Oct 9, 2024

@rwgk If we want unit tests, we just need to update clang-tidy and the static analysis in the latest versions will flag it.

That would make it a lot more interesting. @cyyever, did you use a newer clang-tidy? (Previously you just mentioned grepping.)

Jumping ahead, under the assumption that the latest clang-tidy flags the code changed under this PR:

There would be two parts that come to mind:

  • Resolving the clang-tidy errors.
  • Running global testing. (I'd need to ask someone at Google.)

The latter comes to mind because @dalboris wrote:

The first point may or may not be desirable. Could it hide bugs further down the call chain, where now the function calls an overload that mutates the passed reference?

Guessing is hard, global testing usually very conclusive.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants