When the 8087 numerical coprocessor was designed, it was fairly common for languages to perform all floating-point math using the highest-precision type, and only round the result to lower precision when assigning it to a lower-precision variable. In the original C standard, for example, the sequence:
float a = 16777216, b = 0.125, c = -16777216;
float d = a+b+c;
would promote a
and b
to double
, add them, promote c
to double
, add it, and then store the result rounded to float
. Even though it would have been faster in many cases for a compiler to generate code that would perform operations directly on type float
, it was simpler to have a set of floating-point routines which would operate only on type double
, along with routines to convert to/from float
, than to have separate sets of routines to handle operations on float
and double
. The 8087 was designed around that approach to arithmetic, performing all all arithmetic operations using an 80-bit floating-point type [80 bits was probably chosen because:
On many 16- and 32-bit processors, it's faster to work with a 64-bit mantissa and a separate exponent than to work with value which divides a byte between the mantissa and exponent.
It's very difficult to perform computations which are accurate to the full precision of the numerical types one is using; if one is trying to e.g. compute something like log10(x), it's easier and faster to compute a result which is accurate to within 100ulp of an 80-bit type than to compute a result which is accurate to within 1ulp of a 64-bit type, and rounding the former result to 64-bit precision will yield a 64-bit value which is more accurate than the latter.
Unfortunately, future versions of the language changed the semantics of how floating-point types should work; while the 8087 semantics would have been very nice if languages had supported them consistently, if functions f1(), f2(), etc. return type float
, many compiler authors would take it upon themselves to make long double
an alias for the 64-bit double type rather than the compiler's 80-bit type (and provide no other means of creating 80-bit variables), and to arbitrarily evaluate something like:
double f = f1()*f2() - f3()*f4();
in any of the following ways:
double f = (float)(f1()*f2()) - (extended_double)f3()*f4();
double f = (extended_double)f1()*f2() - (float)(f3()*f4());
double f = (float)(f1()*f2()) - (float)(f3()*f4());
double f = (extended_double)f1()*f2() - (extended_double)f3()*f4();
Note that if f3 and f4 return the same values as f1 and f2, respectively, the original expression should clearly return zero, but many of the latter expressions may not. This led to people condemning the "extra precision" of the 8087 even though the last formulation would generally be superior to the third and--with code that used the extended double type appropriately--would rarely be inferior.
In the intervening years, Intel has responded to language's (IMHO unfortunate) trend toward forcing intermediate results to be rounded to the operands' precision by designing their later processors so as to favor that behavior, to the detriment of code which would benefit from using higher precision on intermediate calculations.