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 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 a
•b
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
.