round - setprecision c++
Error al restar números de punto flotante al pasar por 0.0 (4)
El siguiente programa:
#include <stdio.h>
int main()
{
double val = 1.0;
int i;
for (i = 0; i < 10; i++)
{
val -= 0.2;
printf("%g %s/n", val, (val == 0.0 ? "zero" : "non-zero"));
}
return 0;
}
Produce este resultado:
0.8 non-zero
0.6 non-zero
0.4 non-zero
0.2 non-zero
5.55112e-17 non-zero
-0.2 non-zero
-0.4 non-zero
-0.6 non-zero
-0.8 non-zero
-1 non-zero
¿Alguien puede decirme qué está causando el error al restar 0.2 de 0.2? ¿Es esto un error de redondeo o algo más? Lo más importante, ¿cómo puedo evitar este error?
EDIT: parece que la conclusión es no preocuparse por ello, dado que 5.55112e-17 es extremadamente cercano a cero (gracias a @therefromhere para obtener esa información).
Es porque los números de puntos flotantes no se pueden almacenar en la memoria en el valor exacto. Por lo tanto, nunca es seguro usar ==
en valores de coma flotante. Usar el doble aumentará la precisión, pero nuevamente eso no será exacto. La forma correcta de comparar un valor de coma flotante es hacer algo como esto:
val == target; // not safe // instead do this // where EPS is some suitable low value like 1e-7 fabs(val - target) < EPS;
EDITAR: Como se señaló en los comentarios, la razón principal del problema es que 0.2 no se puede almacenar exactamente. Entonces cuando lo resta de algún valor, cada vez causa un error. Si realiza este tipo de cálculo de punto flotante repetidamente, entonces en cierto punto el error será notable. Lo que trato de decir es que no se pueden almacenar todos los valores de puntos flotantes, ya que hay infinitos de ellos. Por lo general, no se nota un ligero valor incorrecto, pero si se utiliza el cálculo sucesivo se generará un error acumulativo más alto.
La aritmética de coma flotante no puede representar todos los números exactamente. Por lo tanto, los errores de redondeo como usted observan son inevitables.
Una estrategia posible es usar un formato de punto fijo, por ejemplo, un tipo de datos decimal o de moneda. Dichos tipos aún no pueden representar todos los números, pero se comportarían como esperabas para este ejemplo.
Para elaborar un poco: si la mantisa del número de coma flotante está codificada en binario (como es el caso en la mayoría de las FPU contemporáneas), entonces solo sumas de (múltiplos) de los números 1/2, 1/4, 1/8, 1/16, ... se puede representar exactamente en la mantisa. El valor 0.2 es aproximado con 1/8 + 1/16 + ... algunos números incluso más pequeños, sin embargo, el valor exacto de 0.2 no se puede alcanzar con una mantisa finita.
Puedes probar lo siguiente:
printf("%.20f", 0.2);
y probablemente verás que lo que crees que es 0.2 no es 0.2, sino un número que es una cantidad pequeña (en realidad, en mi computadora imprime 0.20000000000000001110). Ahora entiendes por qué nunca puedes llegar a 0.
Pero si permites que val = 12.5 y restas 0.125 en tu ciclo, podrías llegar a cero.
0.2 no es un número de punto flotante de doble precisión, por lo que se redondea al número de precisión doble más cercano, que es:
0.200000000000000011102230246251565404236316680908203125
Eso es bastante difícil de manejar, así que veámoslo en hex:
0x0.33333333333334
Ahora, sigamos lo que sucede cuando este valor se resta repetidamente de 1.0:
0x1.00000000000000
- 0x0.33333333333334
--------------------
0x0.cccccccccccccc
El resultado exacto no es representable con doble precisión, por lo que es redondeado, lo que da:
0x0.ccccccccccccd
En decimal, esto es exactamente:
0.8000000000000000444089209850062616169452667236328125
Ahora repetimos el proceso:
0x0.ccccccccccccd
- 0x0.33333333333334
--------------------
0x0.9999999999999c
rounds to 0x0.999999999999a
(0.600000000000000088817841970012523233890533447265625 in decimal)
0x0.999999999999a
- 0x0.33333333333334
--------------------
0x0.6666666666666c
rounds to 0x0.6666666666666c
(0.400000000000000077715611723760957829654216766357421875 in decimal)
0x0.6666666666666c
- 0x0.33333333333334
--------------------
0x0.33333333333338
rounds to 0x0.33333333333338
(0.20000000000000006661338147750939242541790008544921875 in decimal)
0x0.33333333333338
- 0x0.33333333333334
--------------------
0x0.00000000000004
rounds to 0x0.00000000000004
(0.000000000000000055511151231257827021181583404541015625 in decimal)
Por lo tanto, vemos que el redondeo acumulado que requiere la aritmética de coma flotante produce el muy pequeño resultado distinto de cero que está observando. Redondeo es sutil, pero es determinista, no mágico, y no es un error. Vale la pena tomarse el tiempo para aprender.