8.6 Floating-point issues
The IEEE-754 standard defines the bit-level behavior of
floating-point arithmetic operations on all modern processors.
This allows numerical programs to be ported between different
platforms with identical results, in principle. In practice,
there are often minor variations caused by differences in the
order of operations (depending on the compiler and
optimization level) but these are generally not significant.
However, more noticeable discrepancies can be seen when
porting numerical programs between x86 systems and other
platforms, because the the x87 floating point unit (FPU) on
x86 processors computes results using extended precision
internally (the values being converted to double precision
only when they are stored to memory). In contrast, processors
such as SPARC, PA-RISC, Alpha, MIPS and POWER/PowerPC work
with native double-precision values throughout.(26) The differences between these
implementations lead to changes in rounding and
underflow/overflow behavior, because intermediate values have
a greater relative precision and exponent range when computed
in extended precision.(27) In particular, comparisons involving
extended precision values may fail where the equivalent double
precision values would compare equal.
To avoid these incompatibilities, the x87 FPU also offers a
hardware double-precision rounding mode. In this mode the
results of each extended-precision floating-point operation
are rounded to double precision in the floating-point
registers by the FPU. It is important to note that the
rounding only affects the precision, not the exponent range,
so the result is a hybrid double-precision format with an
extended range of exponents.
On BSD systems such as FreeBSD, NetBSD and OpenBSD, the
hardware double-precision rounding mode is the default, giving
the greatest compatibility with native double precision
platforms. On x86 GNU/Linux systems the default mode is
extended precision (with the aim of providing increased
accuracy). To enable the double-precision rounding mode it is
necessary to override the default setting on per-process basis
using the FLDCW "floating-point load control-word" machine
instruction.(28) A simple function which can be
called to execute this instruction is shown below. It uses the
GCC extension keyword asm to insert the specified
instruction in the assembly language output:
void
set_fpu (unsigned int mode)
{
asm ("fldcw %0" : : "m" (*&mode));
}
The appropriate mode setting for
double-precision rounding is 0x27F. The mode
value also controls the floating-point exception handling
behavior and rounding-direction (see the Intel and AMD
processor reference manuals for details).
On x86 GNU/Linux, the function above can be called at the
start of any program to disable excess precision. This will
then reproduce the results of native double-precision
processors, in the absence of underflows and overflows.
The following program demonstrates the different rounding
modes:
#include <stdio.h>
void
set_fpu (unsigned int mode)
{
asm ("fldcw %0" : : "m" (*&mode));
}
int
main (void)
{
double a = 3.0, b = 7.0, c;
#ifdef DOUBLE
set_fpu (0x27F); /* use double-precision rounding */
#endif
c = a / b;
if (c == a / b) {
printf ("comparison succeeds\n");
} else {
printf ("unexpected result\n");
}
return 0;
}
On x86 GNU/Linux systems the comparison c == a /
b can produce an unexpected result if c is
taken from memory (double precision) while a / b
is computed in extended precision, because the fraction 3/7
has different representations in double and extended
precision.
$ gcc -Wall fptest.c
$ ./a.out
unexpected result
Setting the hardware rounding mode to double precision
prevents this from happening:
$ gcc -Wall -DDOUBLE fptest.c
$ ./a.out
comparison succeeds
Note that the floating-point control word affects the whole
environment of the process, including any C Library functions
that are called. One consequence of this is that long
double arithmetic is effectively reduced to double
precision, since it relies on extended precision operations.
The floating point control word only affects the behavior
of the x87 FPU. Floating point operations computed with SSE
and SSE2 instructions are always carried out in native double
precision. Thus, the combined options
$ gcc -Wall -msse2 -mfpmath=sse ...
are often sufficient to remove the effects of
extended-precision. However, some operations (such as
transcendental functions) are not available in the SSE/SSE2
extensions and will still be computed on the x87 FPU.