c++ - significativas - tecnicas de redondeo
¿Por qué agregar 0 al final del literal flotante cambia la forma en que se redondea(posible error de GCC)? (4)
Descubrí en mi x86 VM (32 bits) que el siguiente programa:
#include <stdio.h>
void foo (long double x) {
int y = x;
printf("(int)%Lf = %d/n", x, y);
}
int main () {
foo(.9999999999999999999728949456878623891498136799780L);
foo(.999999999999999999972894945687862389149813679978L);
return 0;
}
Produce la siguiente salida:
(int)1.000000 = 1
(int)1.000000 = 0
Ideone también produce este comportamiento.
¿Qué está haciendo el compilador para permitir que esto suceda?
Encontré esta constante ya que estaba rastreando por qué el siguiente programa no produjo 0
como esperaba (usando 19 9
s produjo el 0
que esperaba):
int main () {
long double x = .99999999999999999999L; /* 20 9''s */
int y = x;
printf("%d/n", y);
return 0;
}
Cuando traté de calcular el valor en el que el resultado cambia de esperado a inesperado, llegué a la constante de la que trata esta pregunta.
El compilador utiliza números binarios. La mayoría de los compiladores hacen lo mismo.
Según wolframalpha, representación binaria de
0.99999999999999999999
Se ve como esto:
0.11111111111111111111111111111111111111111111111111111111111111111101000011000110101111011110011011011011011110111011100101000101010111011100001011010001001110001101011001010000110000101001111011111001111110000101010111111110100110000010001001101011001101010110110010010101101111101001110001100111101100000000100110110001100110000011000100100011000011110100001000000100001000101000111011010111111101011010010000010110011111110100100110001011001110100011100001111101011110101001000000111110010000101101001001010110010011001110111111100111101111100000111010001101101011000100110001010010001000100010110000101110100101010101001010100010001001100111111111001001101100000000010010001011110100101011101001001101001111001001000101011101001100111101110111111001101110100111000001111101101101101101110100100111101000000000111101101101001000111101100010101110011101110001110010110110111101000011110110100011000110101100011111111110111000010010001111000000000101100101000100101110100001001101000010110101000100011100000110010001110101...
Eso es 932 bits, y STILL no es suficiente para representar con precisión su número (vea los puntos al final).
Lo que significa que mientras su plataforma subyacente utilice la base de 2 para almacenar números, no podrá almacenar exactamente 0.99999999999999999999
.
Debido a que el número no se puede almacenar con precisión, se redondeará hacia arriba o hacia abajo. Con 20 9s termina siendo redondeado hacia arriba, y con 19 9s termina siendo redondeado hacia abajo.
Para evitar este problema, en lugar de los dobles, deberá usar algún tipo de biblioteca de terceros / matemáticas que almacene los números internamente utilizando la base decimal (es decir, dos dígitos decimales por byte o algo) o utilice fracciones (proporciones) en lugar de punto flotante números. Eso solucionaría tu problema.
Hay dos conversiones involucradas aquí. Primero, y de alguna manera lo más importante, es la conversión del literal .99999999999999999999L
a doble largo. Como han dicho otros, esta conversión se redondea al valor representable más cercano, que parece ser 1.0L
. La segunda conversión es del valor doble largo que resultó de la primera conversión a un valor entero. Esa conversión se redondea hacia 0, por lo que un examen rápido sugiere que el valor de y
debería ser 0. Pero debido a que la primera conversión produjo 1 y no un valor ligeramente menor que 1, esta conversión también produce 1.
Los valores dobles, cuando no hay suficiente precisión para representar un valor, se redondean hacia arriba o hacia abajo al más cercano. En su implementación se redondea a 1.
Su problema es que el long double
en su plataforma no tiene precisión suficiente para almacenar el valor exacto 0.99999999999999999999. Esto significa que el valor de eso se debe convertir en un valor representable (esta conversión ocurre durante la traducción de su programa, no en tiempo de ejecución).
Esta conversión puede generar el valor representable más cercano o el siguiente valor representable mayor o menor. La elección está definida por la implementación, por lo que su implementación debe documentar qué está utilizando. Parece que su implementación utiliza el long double
80 bits de estilo x87 y se redondea al valor más cercano, lo que da como resultado un valor de 1.0 almacenado en x
.
Con el formato asumido para el long double
(con 64 bits de mantisa), el número más alto representable menor que 1.0 es, en hexadecimal:
0x0.ffffffffffffffff
El número exactamente a medio camino entre este valor y el siguiente número representable más alto (1.0) es:
0x0.ffffffffffffffff8
Su constante muy larga 0.9999999999999999999728949456878623891498136799780 es igual a:
0x0.ffffffffffffffff7fffffffffffffffffffffffa1eb2f0b64cf31c113a8ec...
que obviamente debería redondearse hacia abajo si se redondea al más cercano, pero parece que ha alcanzado algún límite de la representación de punto flotante que está usando su compilador, o un error de redondeo.