c++gccfloating-pointcompiler-bug

How can a big number fit precisely into `double` with 32-bit GCC?


Consider the following code:

#include <iostream>
int main() {
    long long x = 123456789123456789;
    std::cout << std::fixed;
    auto y = static_cast<double>(x);  // (1)
    std::cout << static_cast<long long>(y) << "\n";  // (2)
    std::cout << y << "\n";
    std::cout << (x == static_cast<long long>(y)) << "\n";  // (3)
    std::cout << static_cast<long long>(static_cast<double>(x)) << "\n";  // (4)
    std::cout << (x == static_cast<long long>(static_cast<double>(x))) << "\n";  // (5)
}

When compiled with 32-bit GCC on Linux (g++ -m32 a.cpp), it prints as follows:

123456789123456784
123456789123456784.000000
0
123456789123456789
1

Note that result of converting long long to double and then back to long long is different depending on how it was done. If I do it via a separate variable double y (lines (1) and (2)), the result ends with 4. But if I do everything in one expression (line (4)), the result ends with 9 just as the original value.

This is quite inconvenient: there exists no double that results in 123456789123456789 when converted to long long, and the check in line (3) confirms that. However, the check in the line (5) passes as if there is one. Is it a bug in GCC or my program?

This behavior started in GCC 9 according to Godbolt link above, GCC 8 works fine. Even funnier, if I add -O2, all expressions are optimized away during compilation and the output is:

123456789123456784
123456789123456784.000000
0
123456789123456784
0

And if I read x from std::cin and keep -O2, the intermediate variable ends in 4, but after conversion to long long a wild 9 appears.

123456789123456789
123456789123456784.000000
1
123456789123456789
1

I believe there is no undefined behavior in the program above:


Solution

  • GCC does document this non-standard behavior in https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html.

    When no -std=c++XX option is given (i.e. if a default -std=gnu++XX is used), then -fexcess-precision is set to fast.

    The effect of this is that, in violation of the C and C++ standards, GCC assumes that operations can always be carried out in higher precision than the types permit and whether this will happen in any given instance is unspecified.

    The C and C++ standards however require casts and assignment to round to a value representable in the actual target type. Therefore the result of your checks is not permitted to be 1 as you describe. (However, for other, i.e. arithmetic, operations in a single expression the standards do explicitly permit operation in higher precision.)

    You get the standard-conforming behavior with -fexcess-precision=standard which is automatically enabled with a -std=c++XX option. However, for C++, it has been implemented only since GCC 13.

    Similarly GCC defaults to -ffp-contract=fast, which is also non-conforming and permits GCC to contract floating point operations across statements, i.e. to assume infinite precision intermediate results, while the standards again don't permit this across statements, casts or assignment. The standard-conforming option is -ffp-contract=on (or -ffp-contract=off to completely disable it).

    (This however doesn't mean that there aren't still bugs that make the behavior non-conforming as discussed in the bug reports linked in the other answer.)