-
-
Notifications
You must be signed in to change notification settings - Fork 193
Use perfect forwarding for functions that use apply_*_*
functions
#3215
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
base: develop
Are you sure you want to change the base?
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.
Overall big fan of the changes! Just a couple of general queries before a full review
stan/math/fwd/fun/log_softmax.hpp
Outdated
log_softmax_alpha(k).d_ += negative_alpha_m_d_times_softmax_alpha_t_m; | ||
inline auto log_softmax(T&& x) { | ||
return apply_vector_unary<T>::apply( | ||
std::forward<T>(x), [&](const auto& alpha) { |
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.
std::forward<T>(x), [&](const auto& alpha) { | |
std::forward<T>(x), [](auto&& alpha) { |
The apply_vector_unary
functors themselves should probably also perfect-forwarding, since they'll be passing their inputs to apply_*
functions as well.
Also probably best to remove the reference-capture default while we're here
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 apply_vector_unary functors themselves should probably also perfect-forwarding, since they'll be passing their inputs to apply_* functions as well.
Can you clarify? I'm not seeing in this function how much forwarding can be done. I do the perfect forwarding in the actual code for apply_vector_unary
etc if that is was you mean.
Also probably best to remove the reference-capture default while we're here
Agree
static inline auto apply(const T2& x) { | ||
return F::fun(x); | ||
} |
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.
static inline auto apply(const T2& x) { | |
return F::fun(x); | |
} | |
static inline auto apply(T2&& x) { | |
return F::fun(std::forward<T2>(x)); | |
} |
Should this also be forwarding? As the downstream calls are forwarding their arguments to apply_scalar_unary<*>::apply()
return apply_vector_unary<T>::apply( | ||
y, [K](auto&& v) { return cholesky_corr_constrain(v, K); }); | ||
inline auto cholesky_corr_constrain(T&& y, int K) { | ||
return apply_vector_unary<std::decay_t<T>>::apply( |
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.
Not something you necessarily need to change in this PR, but it might be a bit cleaner to move the std::decay_t<T>
handling into apply_vector_unary
itself
@@ -43,7 +43,7 @@ inline plain_type_t<T> corr_constrain(const T& x) { | |||
* @param[in,out] lp log density accumulator | |||
*/ | |||
template <typename T_x, typename T_lp> | |||
inline auto corr_constrain(const T_x& x, T_lp& lp) { | |||
inline auto corr_constrain(T_x&& x, T_lp& lp) { | |||
plain_type_t<T_x> tanh_x = tanh(x); |
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.
plain_type_t<T_x> tanh_x = tanh(x); | |
plain_type_t<T_x> tanh_x = tanh(std::forward<T_x>(x)); |
@@ -34,7 +34,7 @@ namespace math { | |||
*/ | |||
template <typename T, typename L, require_all_stan_scalar_t<T, L>* = nullptr, | |||
require_all_not_st_var<T, L>* = nullptr> | |||
inline auto lb_constrain(const T& x, const L& lb) { | |||
inline auto lb_constrain(T&& x, const L& lb) { |
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.
Looks like the the std::forward<T>(x)
is missing from this function, and there are other container overloads in the file which probably need forwarding added as well
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.
So for functions that just operate on scalars I don't think we need to worry about forwarding as much since those functions will immediately evaluate
require_not_std_vector_t<M>* = nullptr> | ||
inline auto offset_multiplier_free(const std::vector<T>& x, const M& mu, | ||
const std::vector<S>& sigma) { | ||
inline auto offset_multiplier_free(T&& x, const M& mu, S&& sigma) { |
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 think mu
also needs to be forwarded here since it's passed to to_ref
below
divide(subtract(std::forward<T>(y), std::forward<M>(mu_ref)), | ||
std::forward<S>(sigma_ref))); |
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.
divide(subtract(std::forward<T>(y), std::forward<M>(mu_ref)), | |
std::forward<S>(sigma_ref))); | |
divide(subtract(std::forward<T>(y), std::forward<decltype(mu_ref)>(mu_ref)), | |
std::forward<decltype(sigma_ref)>(sigma_ref))); |
auto&& mu_ref = to_ref(std::forward<M>(mu)); | ||
auto&& sigma_ref = to_ref(std::forward<S>(sigma)); |
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.
Should these also be forwarded in the offset_multiplier_free
call below?
lp += log_inv_logit_x + log1m_inv_logit(x); | ||
return exp(log_inv_logit_x); | ||
inline auto prob_constrain(T&& x, return_type_t<T>& lp) { | ||
plain_type_t<T> log_inv_logit_x = log_inv_logit(x); |
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.
plain_type_t<T> log_inv_logit_x = log_inv_logit(x); | |
plain_type_t<T> log_inv_logit_x = log_inv_logit(std::forward<T>(x)); |
Re #3208, this branch fixes the issue in math/stan/math/prim/prob/loglogistic_cdf.hpp Lines 115 to 116 in f7ccc01
Could be related to #3147? |
…arried through them
I do think we should weigh the odds that merging something of this scope during a release window would introduce more issues than it resolves (especially because the issues seem to have been present in the code for a couple years) |
I agree this is a large PR. Once I found the bug in one place I realized it was kind of systematic to all of our functions that can take in an expression and then output an expression. Let's talk irl. I agree this is risky before a release, but also think the bug is kind of large and can also happen in user space |
Not denying it can happen to users, but as far as we know it hasn’t in the couple years it has been present. So, would it be that bad to hold the fix a couple months for the next release Anyway, especially if @andrjohns thinks he can review it, I’m not opposed to merging, just wanted to ask in the name of due diligence |
Yes I agree that it feels risky. But this is a very odd bug and imo I don't feel great about having this in a release when we know it is there. |
Summary
This fixes #3208 by using perfect forwarding for all functions that use our underlying apply family of functions for calling functions on containers and containers and containers. The issue was that the
Holder
class, when used in the apply functors, did not have enough type information to know which arguments it should take ownership of.Consider the following function, where all types are passed in via constant reference.
Calling this function with an Eigen expression that has a temporary in it would not give
apply_scalar_binary
and theHolder
inside ofapply_scalar_binary
enough information to know that theHolder
class should own any of the input arguments. As an example we can look at a simplified version of the code used inpoisson_lccdf.hpp
.gamma_p
usesapply_scalar_binary
andlog
usesapply_scalar_unary
. We need to make sure the inputs and results of thegamma_p
function do not fall out of scope by the time we go throughlog
and then assign tolog_Pi
. Before this PR it would be possible for the expressionn_val + 1.0
to fall out of scope as well as the result ofgamma_p
to go out of scope fromlog
afterlog_Pi
is assigned.To combat this we now use perfect forwarding for all of the functions that use our internal
apply
family of functors. This should allow theHolder
used internally by the apply functors to know which types need to be owned by it to make sure things do not fall out of scope.Tests
There is no new tests for this. Since it is an isue on gcc I do wonder how we should test this in our CI/CD?
Side Effects
I'd like to think of some test we can write so that, in the future, developers do not accidentally write functions that use the apply family of functors that do not use perfect forwarding.
Release notes
Adds perfect forwarding to all functions that use the apply family of functors.
Checklist
Copyright holder: Steve Bronder
The copyright holder is typically you or your assignee, such as a university or company. By submitting this pull request, the copyright holder is agreeing to the license the submitted work under the following licenses:
- Code: BSD 3-clause (https://opensource.org/licenses/BSD-3-Clause)
- Documentation: CC-BY 4.0 (https://creativecommons.org/licenses/by/4.0/)
the basic tests are passing
./runTests.py test/unit
)make test-headers
)make test-math-dependencies
)make doxygen
)make cpplint
)the code is written in idiomatic C++ and changes are documented in the doxygen
the new changes are tested