-
Notifications
You must be signed in to change notification settings - Fork 13.4k
Incorrect overflow in std::hypot #92782
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
Comments
When fixing this issue one could also look at the symmetrical problem for underflows, e.g. #include <cmath>
float f = std::hypot(1e-30f, 1e-30f, 1e-30f); This yields |
The current implementation uses a literal translation of the mathematical definition, e.g. float hypot(float a, float b, float c) { return std::sqrt(a*a + b*b + c*c); } I would propose a version which scales the arguments. Similar to the following: #include <algorithm> // std::max
float hypot(float a, float b, float c) {
// ... todo: handle infs, NaNs
const float M = std::max({std::abs(a), std::abs(b), std::abs(c)});
if (M == 0)
return 0;
else
return M * std::sqrt( std::pow(a/M, 2) + std::pow(b/M, 2) + std::pow(c/M, 2) );
} Note, more thought needs to be put into handling input validation for infinities and/or NaNs. Futhermore, one could take a look at new possibilities for underflowing of intermediate terms, e.g. |
See also https://github.com/llvm/llvm-project/blob/main/libc/src/math/generic/hypotf.cpp
The underflow doesn't really matter in this case: |
This scaling is expensive and incurs extra rounding, also I think it's better to not call if (M > 0x1.0p511) {
// a*a + b*b + c*c might overflow double precision
a *= 0x1.0p-600; // scale down
b *= 0x1.0p-600; // scale down
c *= 0x1.0p-600; // scale down
M = 0x1.0p600; // scale up factor
} else if (M < 0x1.0p-511) {
// a*a, b*b, c*c might underflow double precision
a *= 0x1.0p600; // scale up
b *= 0x1.0p600; // scale up
c *= 0x1.0p600; // scale up
M = 0x1.0p-600; // scale down factor
} else {
M = 1.0;
}
return M * sqrt(a*a + b*b + c*c); |
I will try to create a patch similar to your proposed version. For that purpose I will try to give an explanation for the "magic-numbers". A double precision IEEE 754 has a range for exponents of [-1022, 1023]. (Biased 11-bit exponent.) The numbers a,b,c squared should not exceed those bounds. This explains 0x1.0p+-511. The scaling factor needs to be between +-511 and the exponent bounds. This explains 0x1.0p600. Similar arguments can be made for floats and long double. |
The 3-dimentionsional `std::hypot(x,y,z)` was sub-optimally implemented. This lead to possible over-/underflows in (intermediate) results which can be circumvented by this proposed change. The idea is to to scale the arguments (see linked issue for full discussion). Tests have been added for problematic over- and underflows. Closes llvm#92782 (cherry picked from commit 9628777)
The 3-dimentionsional `std::hypot(x,y,z)` was sub-optimally implemented. This lead to possible over-/underflows in (intermediate) results which can be circumvented by this proposed change. The idea is to to scale the arguments (see linked issue for full discussion). Tests have been added for problematic over- and underflows. Closes llvm#92782 (cherry picked from commit 9628777)
Summary: The 3-dimentionsional `std::hypot(x,y,z)` was sub-optimally implemented. This lead to possible over-/underflows in (intermediate) results which can be circumvented by this proposed change. The idea is to to scale the arguments (see linked issue for full discussion). Tests have been added for problematic over- and underflows. Closes #92782 Test Plan: Reviewers: Subscribers: Tasks: Tags: Differential Revision: https://phabricator.intern.facebook.com/D60251315
@ldionne This should be reopened as the fix was reverted. Note, I just opened another MR which would close this again if it gets merged.. |
This is in relation to mr #93350. It was merged to main, but reverted because of failing sanitizer builds on PowerPC. The fix includes replacing the hard-coded threshold constants (e.g. `__overflow_threshold`) for different floating-point sizes by a general computation using `std::ldexp`. Thus, it should now work for all architectures. This has the drawback of not being `constexpr` anymore as `std::ldexp` is not implemented as `constexpr` (even though the standard mandates it for C++23). Closes #92782
) This is in relation to mr llvm#93350. It was merged to main, but reverted because of failing sanitizer builds on PowerPC. The fix includes replacing the hard-coded threshold constants (e.g. `__overflow_threshold`) for different floating-point sizes by a general computation using `std::ldexp`. Thus, it should now work for all architectures. This has the drawback of not being `constexpr` anymore as `std::ldexp` is not implemented as `constexpr` (even though the standard mandates it for C++23). Closes llvm#92782 (cherry picked from commit 72825fd)
) This is in relation to mr llvm#93350. It was merged to main, but reverted because of failing sanitizer builds on PowerPC. The fix includes replacing the hard-coded threshold constants (e.g. `__overflow_threshold`) for different floating-point sizes by a general computation using `std::ldexp`. Thus, it should now work for all architectures. This has the drawback of not being `constexpr` anymore as `std::ldexp` is not implemented as `constexpr` (even though the standard mandates it for C++23). Closes llvm#92782 (cherry picked from commit 72825fd)
Consider:
With libc++, this produces +inf. Other libraries appear to handle this correctly. (The C standard suggests hypot() should return a result "without undue overflow or underflow"... although that only applies to the two-argument form).
The text was updated successfully, but these errors were encountered: