Skip to content

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

Closed
efriedma-quic opened this issue May 20, 2024 · 7 comments · Fixed by #93350 or #100820
Closed

Incorrect overflow in std::hypot #92782

efriedma-quic opened this issue May 20, 2024 · 7 comments · Fixed by #93350 or #100820
Labels
libc++ libc++ C++ Standard Library. Not GNU libstdc++. Not libc++abi.

Comments

@efriedma-quic
Copy link
Collaborator

Consider:

#include <cmath>
float f = std::hypot(1e20f,1e20f,1e20f);

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).

@efriedma-quic efriedma-quic added the libc++ libc++ C++ Standard Library. Not GNU libstdc++. Not libc++abi. label May 20, 2024
@PaulXiCao
Copy link
Contributor

PaulXiCao commented May 20, 2024

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 0 for libc++ and the correct non-zero result for libstdc++. See this godbolt which shows clang using both standard libraries.

@PaulXiCao
Copy link
Contributor

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. hypot(1e-15f, 1e15f, 1e15f) for which a*a did not underflow, but std::pow(a/M, 2) does.

@efriedma-quic
Copy link
Collaborator Author

See also https://github.com/llvm/llvm-project/blob/main/libc/src/math/generic/hypotf.cpp

e.g. hypot(1e-15f, 1e15f, 1e15f) for which a*a did not underflow, but std::pow(a/M, 2) does.

The underflow doesn't really matter in this case: hypot(0, 1e15f, 1e15f) produces the same result. It's only relevant if all the operands are small.

@lntue
Copy link
Contributor

lntue commented May 21, 2024

   return M * std::sqrt( std::pow(a/M, 2) + std::pow(b/M, 2) + std::pow(c/M, 2) );

This scaling is expensive and incurs extra rounding, also I think it's better to not call std::pow. To prevent overflow and underflow, we can use some fixed scaling factor that is exact to perform:

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);

@PaulXiCao
Copy link
Contributor

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.

@ldionne ldionne added this to the LLVM 19.X Release milestone Jul 9, 2024
llvmbot pushed a commit to llvmbot/llvm-project that referenced this issue Jul 23, 2024
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)
tru pushed a commit to llvmbot/llvm-project that referenced this issue Jul 24, 2024
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)
yuxuanchen1997 pushed a commit that referenced this issue Jul 25, 2024
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
@PaulXiCao
Copy link
Contributor

@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..

@mordante mordante reopened this Jul 27, 2024
ldionne pushed a commit that referenced this issue Aug 5, 2024
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
ldionne pushed a commit to ldionne/llvm-project that referenced this issue Aug 5, 2024
)

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)
tru pushed a commit to ldionne/llvm-project that referenced this issue Aug 13, 2024
)

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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
libc++ libc++ C++ Standard Library. Not GNU libstdc++. Not libc++abi.
Projects
None yet
5 participants