Home > Blockchain >  Should two programs compiled with -O0 and -O2 each produce identical floating point results?
Should two programs compiled with -O0 and -O2 each produce identical floating point results?

Time:10-22

Short example:

#include <iostream>
#include <string_view>
#include <iomanip>

#define PRINTVAR(x) printVar(#x, (x) )

void printVar( const std::string_view name, const float value )
{
    std::cout 
        << std::setw( 16 )
        << name 
        << " = " << std::setw( 12 ) 
        << value << std::endl;
}

int main()
{
    std::cout << std::hexfloat;
    
    const float x = []() -> float
    {
        std::string str;
        std::cin >> str; //to avoid 
                         //trivial optimization
        return strtof(str.c_str(), nullptr);
    }();

    const float a = 0x1.bac178p-5;
    const float b = 0x1.bb7276p-5;

    const float x_1 = (1 - x);

    PRINTVAR( x );
    PRINTVAR( x_1 );
    PRINTVAR( a );
    PRINTVAR( b );

    PRINTVAR( a * x_1   b * x );
    return 0;
}

this code on godbolt

This code produces different output on different platforms/compilers/optimizations:

X = 0x1.bafb7cp-5 //this is float in the std::hexfloat notation
Y = 0x1.bafb7ep-5

The input value is always the same: 0x1.4fab12p-2

compiler optimization x86_64 aarch64
GCC-12.2 -O0 X X
GCC-12.2 -O2 X Y
Clang-14 -O0 X Y
Clang-14 -O2 X Y

As we can see, Clang gives us identical results between -O0 and -O2 within same architecture, but GCC does not.

The question is - should we expect the identical result with -O0 and -O2 on the same platform?

CodePudding user response:

The question is - should we expect the identical result with -O0 and -O2 on the same platform?

No, not in general.

C 2020 draft N4849 7.1 [expr.pre] 6 says:

The values of the floating-point operands and the results of floating-point expressions may be represented in greater precision and range than that required by the type; the types are not changed thereby.51

Footnote 51 says:

The cast and assignment operators must still perform their specific conversions as described in 7.6.1.3, 7.6.3, 7.6.1.8 and 7.6.19.

This means that while evaluating a * x_1 b * x, the C implementation may use the nominal float type of the operands or it may use any “superset” format with greater precision and/or range. That could be double or long double or an unnamed format. Once the evaluation is complete, and the result is assigned to a variable (including, in your example, a function parameter), the result calculated with extended precision must be converted to a value representable in the float type. So you will always see a float result, but it may be a different result than if the arithmetic were performed entirely with the float type.

The C standard does not require the C implementation to make the same choice about what precision it uses in all instances. Even if it did, each combination of command-line switches to the compiler may be regarded a different C implementation (at least for the switches that may affect program behavior). Thus the C implementation obtained with -O0 may use float arithmetic throughout while the C implementation obtained with -O2 may use extended precision.

Note that the extended precision used to calculate may be obtained not just through the use of machine instructions for a wider type, such as instructions that operate on double values rather than float values, but may arise through instructions such as a fused multiply-add, which computes a*b c as if ab c were computed with infinite precision and then rounded to the nominal type. This avoids the rounding error that would occur if a*b were computed first, producing a float result, and then added to c.

  • Related