c# - name - (.1f+.2f==. 3f)!=(.1f+.2f).Equals(.3f) ¿Por qué?
remarks c# (5)
Como se dijo en los comentarios, esto se debe a que el compilador realiza una propagación constante y realiza el cálculo con mayor precisión (creo que esto depende de la CPU).
var f1 = .1f + .2f;
var f2 = .3f;
Console.WriteLine(f1 == f2); // prints true (same as Equals)
Console.WriteLine(.1f+.2f==.3f); // prints false (acts the same as double)
@ Caramiriel también señala que .1f+.2f==.3f
se emite como false
en el IL, por lo que el compilador hizo el cálculo en tiempo de compilación.
Para confirmar la optimización constante del compilador plegado / propagación
const float f1 = .1f + .2f;
const float f2 = .3f;
Console.WriteLine(f1 == f2); // prints false
Mi pregunta no es acerca de la precisión flotante. Se trata de por qué Equals()
es diferente de ==
.
Entiendo por qué .1f + .2f == .3f
es false
(mientras que .1m + .2m == .3m
es true
).
Entiendo que ==
es referencia y .Equals()
es comparación de valores. ( Editar : Sé que hay más en esto).
Pero ¿por qué es (.1f + .2f).Equals(.3f)
true
, mientras que (.1d+.2d).Equals(.3d)
sigue siendo false
?
.1f + .2f == .3f; // false
(.1f + .2f).Equals(.3f); // true
(.1d + .2d).Equals(.3d); // false
Cuando escribes
double a = 0.1d;
double b = 0.2d;
double c = 0.3d;
En realidad , estos no son exactamente 0.1
, 0.2
y 0.3
. Del código IL;
IL_0001: ldc.r8 0.10000000000000001
IL_000a: stloc.0
IL_000b: ldc.r8 0.20000000000000001
IL_0014: stloc.1
IL_0015: ldc.r8 0.29999999999999999
Hay una gran cantidad de preguntas en SO que apuntan a un problema como ( Diferencia entre decimal, flotante y doble en .NET y Tratamiento de errores de coma flotante en .NET ), pero sugiero que lea un artículo interesante llamado;
What Every Computer Scientist Should Know About Floating-Point Arithmetic
Bueno , lo que said Leppie es más lógico. La situación real está aquí, depende totalmente del compiler
/ computer
o cpu
.
Basado en el código Leppie, este código funciona en mi Visual Studio 2010 y Linqpad , como resultado True
/ False
, pero cuando lo probé en ideone.com , el resultado será True
/ True
Verifica la DEMO .
Consejo : Cuando escribí Console.WriteLine(.1f + .2f == .3f);
Resharper me advierte;
Comparación del número de puntos flotantes con el operador de igualdad. Posible pérdida de precisión al redondear valores.
FWIW siguiendo los pases de prueba
float x = 0.1f + 0.2f;
float result = 0.3f;
bool isTrue = x.Equals(result);
bool isTrue2 = x == result;
Assert.IsTrue(isTrue);
Assert.IsTrue(isTrue2);
Entonces el problema es en realidad con esta línea
0.1f + 0.2f == 0.3f
Que como se dijo es probablemente un compilador / pc específico
La mayoría de la gente está saltando a esta pregunta desde un ángulo equivocado, creo que hasta ahora
ACTUALIZAR:
Otra prueba curiosa, creo
const float f1 = .1f + .2f;
const float f2 = .3f;
Assert.AreEqual(f1, f2); passes
Assert.IsTrue(f1==f2); doesnt pass
Implementación de igualdad única:
public bool Equals(float obj)
{
return ((obj == this) || (IsNaN(obj) && IsNaN(this)));
}
La pregunta es confusamente redactada. Vamos a dividirlo en muchas preguntas más pequeñas:
¿Por qué es que una décima más dos décimas no siempre equivale a tres décimas en aritmética de coma flotante?
Dejame darte una analogía. Supongamos que tenemos un sistema matemático donde todos los números se redondean a exactamente cinco decimales. Supongamos que dices:
x = 1.00000 / 3.00000;
Esperaría que x fuera 0.33333, ¿verdad? Porque ese es el número más cercano en nuestro sistema a la respuesta real . Ahora supongamos que dijiste
y = 2.00000 / 3.00000;
Esperas que y sea 0.66667, ¿verdad? Porque nuevamente, ese es el número más cercano en nuestro sistema a la respuesta real . 0.66666 está más lejos de dos tercios que 0.66667 es.
Note que en el primer caso redondeamos hacia abajo y en el segundo caso redondeamos hacia arriba.
Ahora cuando decimos
q = x + x + x + x;
r = y + x + x;
s = y + y;
¿Qué obtenemos? Si hiciéramos aritmética exacta, entonces cada uno de estos sería obviamente cuatro tercios y todos serían iguales. Pero ellos no son iguales. Aunque 1,333333 es el número más cercano en nuestro sistema a cuatro tercios, solo r tiene ese valor.
q es 1.33332 - porque x era un poco pequeño, cada adición acumuló ese error y el resultado final es bastante pequeño. Del mismo modo, s es demasiado grande; es 1.33334, porque y era un poco demasiado grande. r obtiene la respuesta correcta porque la demasiada grandedad de y se cancela por la pequeñez de x y el resultado termina correcto.
¿El número de lugares de precisión tiene un efecto sobre la magnitud y la dirección del error?
Sí; más precisión reduce la magnitud del error, pero puede cambiar si un cálculo acumula una pérdida o una ganancia debido al error. Por ejemplo:
b = 4.00000 / 7.00000;
b sería 0.57143, que redondea desde el verdadero valor de 0.571428571 ... Si hubiésemos ido a ocho lugares, sería 0.57142857, que tiene una magnitud de error mucho más pequeña, pero en la dirección opuesta; redondeó hacia abajo.
Debido a que cambiar la precisión puede cambiar si un error es una ganancia o una pérdida en cada cálculo individual, esto puede cambiar si los errores de un cálculo agregado dado se refuerzan mutuamente o se cancelan mutuamente. El resultado neto es que a veces un cálculo de menor precisión está más cerca del resultado "verdadero" que un cálculo de precisión más alta porque en el cálculo de precisión más baja se tiene suerte y los errores están en diferentes direcciones.
Es de esperar que hacer un cálculo con mayor precisión siempre proporcione una respuesta más cercana a la respuesta verdadera, pero este argumento muestra lo contrario. Esto explica por qué a veces un cálculo en flotantes da la respuesta "correcta", pero un cálculo en dobles, que tiene el doble de precisión, da la respuesta "incorrecta", ¿correcto?
Sí, esto es exactamente lo que está sucediendo en sus ejemplos, excepto que en lugar de cinco dígitos de precisión decimal tenemos una cierta cantidad de dígitos de precisión binaria . Así como un tercio no se puede representar con precisión en cinco dígitos decimales, o 0.1, 0.1, 0.2 y 0.3 no se pueden representar con precisión en ningún número finito de dígitos binarios. Algunos de ellos se redondearán, algunos de ellos se redondearán hacia abajo, y si las adiciones de ellos aumentan el error o cancelan el error depende de los detalles específicos de cuántos dígitos binarios hay en cada sistema. Es decir, los cambios en la precisión pueden cambiar la respuesta para bien o para mal. En general, cuanto mayor es la precisión, más cerca está la respuesta a la respuesta verdadera, pero no siempre.
¿Cómo puedo obtener cálculos aritméticos decimales precisos, si el flotante y el doble usan dígitos binarios?
Si necesita matemática decimal precisa, entonces use el tipo decimal
; usa fracciones decimales, no fracciones binarias. El precio que paga es que es considerablemente más grande y más lento. Y, por supuesto, como ya hemos visto, fracciones como un tercio o cuatro séptimos no se representarán con precisión. Sin embargo, cualquier fracción que en realidad sea una fracción decimal se representará con cero errores, hasta aproximadamente 29 dígitos significativos.
De acuerdo, acepto que todos los esquemas de punto flotante introducen imprecisiones debido a un error de representación, y que esas imprecisiones a veces pueden acumularse o cancelarse entre sí en función del número de bits de precisión utilizados en el cálculo. ¿Al menos tenemos la garantía de que esas imprecisiones serán consistentes ?
No, no tienes esa garantía para carrozas o dobles. Tanto el compilador como el tiempo de ejecución pueden realizar cálculos de coma flotante con una precisión mayor que la requerida por la especificación. En particular, el compilador y el tiempo de ejecución pueden hacer aritmética de precisión simple (32 bit) en 64 bit o 80 bit o 128 bit o cualquier bitness mayor que 32 que les guste .
El compilador y el tiempo de ejecución pueden hacerlo como lo deseen en ese momento . No necesitan ser consistentes de una máquina a otra, de una ejecución a otra, etc. Como esto solo puede hacer que los cálculos sean más precisos, esto no se considera un error. Es una característica. Una característica que hace que sea increíblemente difícil escribir programas que se comporten de manera predecible, pero una característica sin embargo.
Entonces, ¿eso significa que los cálculos realizados en tiempo de compilación, como los literales 0.1 + 0.2, pueden dar resultados diferentes que el mismo cálculo realizado en tiempo de ejecución con variables?
Sí.
¿Y si comparamos los resultados de
0.1 + 0.2 == 0.3
a(0.1 + 0.2).Equals(0.3)
?
Como el primero es computado por el compilador y el segundo es computado por el tiempo de ejecución, y acabo de decir que se les permite usar arbitrariamente más precisión de la requerida por la especificación a su antojo, sí, pueden dar resultados diferentes. Tal vez uno de ellos elige hacer el cálculo solo con una precisión de 64 bits, mientras que el otro elige la precisión de 80 o 128 bits para una parte o la totalidad del cálculo y obtiene una respuesta diferente.
Así que espera un minuto aquí. Usted está diciendo que no solo que
0.1 + 0.2 == 0.3
puede ser diferente de(0.1 + 0.2).Equals(0.3)
. Usted está diciendo que0.1 + 0.2 == 0.3
se puede calcular como verdadero o falso por completo según el capricho del compilador. Podría producir verdadero los martes y falso los jueves, podría producir verdadero en una máquina y falso en otra, podría producir verdadero y falso si la expresión aparecía dos veces en el mismo programa. Esta expresión puede tener cualquier valor por cualquier motivo; el compilador no puede ser completamente confiable aquí.
Correcto.
La forma en que generalmente se informa al equipo del compilador de C # es que alguien tiene alguna expresión que produce verdadero cuando compilan en depuración y falsa cuando compilan en modo de lanzamiento. Esa es la situación más común en la que esto surge porque la generación del código de depuración y liberación cambia los esquemas de asignación de registros. Pero el compilador tiene permitido hacer lo que quiera con esta expresión, siempre que elija verdadero o falso. (No puede, por ejemplo, producir un error en tiempo de compilación).
Esto es una locura
Correcto.
¿A quién debería culpar por este lío?
Yo no, eso es malditamente seguro.
Intel decidió crear un chip matemático de coma flotante en el que era mucho, mucho más costoso obtener resultados consistentes. Pequeñas opciones en el compilador sobre qué operaciones registrar y qué operaciones mantener en la pila pueden sumar grandes diferencias en los resultados.
¿Cómo me aseguro de resultados consistentes?
Usa el tipo decimal
, como dije antes. O haz todas tus matemáticas en enteros.
Tengo que usar dobles o flotadores; ¿Puedo hacer algo para alentar resultados consistentes?
Sí. Si almacena cualquier resultado en cualquier campo estático , cualquier campo de instancia de una clase o elemento de matriz de tipo float o double, se garantiza que se truncará de nuevo a una precisión de 32 o 64 bits. (Esta garantía no está expresamente hecha para tiendas a locales o parámetros formales). Además, si realiza un molde de tiempo de ejecución para (float)
o (double)
en una expresión que ya es de ese tipo, el compilador emitirá un código especial que forzará la resultado para truncar como si hubiera sido asignado a un campo o elemento de matriz. (Los lanzamientos que se ejecutan en tiempo de compilación, es decir, lanzados en expresiones constantes, no se garantiza que lo hagan).
Para aclarar ese último punto: ¿la especificación del lenguaje C # hace esas garantías?
No. El tiempo de ejecución garantiza que las tiendas en una matriz o campo se truncan. La especificación C # no garantiza que un molde de identidad trunque, pero la implementación de Microsoft tiene pruebas de regresión que aseguran que cada versión nueva del compilador tenga este comportamiento.
Todo lo que la especificación del lenguaje tiene que decir sobre el tema es que las operaciones de coma flotante se pueden realizar con mayor precisión a la discreción de la implementación.
==
se trata de comparar los valores exactos de flotantes.
Equals
es un método booleano que puede devolver verdadero o falso. La implementación específica puede variar.