Cuando se diseñó el coprocesador numérico 8087, era bastante común que los idiomas realizaran todos los cálculos de punto flotante utilizando el tipo de mayor precisión, y solo redondeaban el resultado a una precisión más baja al asignarlo a una variable de menor precisión. En el estándar C original, por ejemplo, la secuencia:
float a = 16777216, b = 0.125, c = -16777216;
float d = a+b+c;
promovería a
y b
a double
, agregarlos, promover c
a double
, agregarlo y luego almacenar el resultado redondeado a float
. Aunque en muchos casos hubiera sido más rápido para un compilador generar código que realizaría operaciones directamente en el tipo float
, era más simple tener un conjunto de rutinas de punto flotante que operaría solo en el tipo double
, junto con con rutinas para convertir a / desde float
, que tener conjuntos separados de rutinas para manejar operaciones en float
y double
. El 8087 se diseñó alrededor de esa aproximación a la aritmética, realizando todas las operaciones aritméticas utilizando un tipo de punto flotante de 80 bits [probablemente se eligió 80 bits porque:
-
En muchos procesadores de 16 y 32 bits, es más rápido trabajar con una mantisa de 64 bits y un exponente separado que trabajar con un valor que divide un byte entre la mantisa y el exponente.
-
Es muy difícil realizar cálculos que sean precisos con la precisión total de los tipos numéricos que se utilizan; si uno está tratando de, por ejemplo, calcular algo como log10 (x), es más fácil y más rápido calcular un resultado que es preciso dentro de 100 pulg de un tipo de 80 bits que calcular un resultado que es preciso dentro de 1 pulso de un tipo de 64 bits, y redondeando el anterior el resultado a una precisión de 64 bits producirá un valor de 64 bits que es más preciso que el último.
Lamentablemente, las versiones futuras del lenguaje cambiaron la semántica de cómo deberían funcionar los tipos de punto flotante; mientras que la semántica 8087 hubiera sido muy agradable si los lenguajes los hubieran apoyado de manera consistente, si las funciones f1 (), f2 (), etc. devolvieran el tipo float
, muchos autores del compilador se encargarían de hacer de long double
un alias el tipo doble de 64 bits en lugar del tipo de 80 bits del compilador (y no proporciona ningún otro medio para crear variables de 80 bits), y para evaluar arbitrariamente algo como:
double f = f1()*f2() - f3()*f4();
de cualquiera de las siguientes maneras:
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();
Tenga en cuenta que si f3 y f4 devuelven los mismos valores que f1 y f2, respectivamente, la expresión original debería devolver cero, pero muchas de las últimas expresiones pueden no hacerlo. Esto condujo a la gente a condenar la "precisión extra" del 8087, aunque la última formulación generalmente sería superior a la tercera y, con el código que usaba el tipo doble extendido, rara vez sería inferior.
En los años intermedios, Intel ha respondido a la tendencia del lenguaje (IMHO desafortunado) de forzar los resultados intermedios para redondearlos a la precisión de los operandos mediante el diseño de sus procesadores posteriores para favorecer ese comportamiento, en detrimento del código que beneficiaría de usar una mayor precisión en cálculos intermedios.