(.1f+.2f==.3f)!= (.1f+.2f).Igual(.3f) ¿Por qué?


Mi pregunta es no 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
Author: Daniel Pelsmaeker, 2013-02-27

5 answers

La pregunta está redactada de manera confusa. Vamos a dividirlo en muchas preguntas más pequeñas:

¿Por qué es que una décima más dos décimas no siempre es igual a tres décimas en la aritmética de coma flotante?

Permítanme darles una analogía. Supongamos que tenemos un sistema matemático donde todos los números se redondean a exactamente cinco decimales. Supongamos que usted dice:

x = 1.00000 / 3.00000;

Esperarías 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 usted dijo

y = 2.00000 / 3.00000;

Esperarías que y fuera 0.66667, ¿verdad? Porque de nuevo, ese es el número más cercano en nuestro sistema a la respuesta real. 0.66666 es más de dos tercios que 0.66667 es.

Observe que en el primer caso redondeamos hacia abajo y en el segundo caso redondeamos hacia arriba.

Ahora cuando decimos{[14]]}

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 no son iguales. Aunque 1.33333 es el número más cercano en nuestro sistema a cuatro tercios, solo r tiene ese valor.

Q es 1.33332 because debido a que x era un poco pequeño, cada adición acumuló ese error y el resultado final es un poco demasiado 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 el demasiado-grande-ness de y se cancela fuera por el demasiado-pequeño-ness de x y el resultado termina siendo correcto.

¿El número de lugares de precisión tiene un efecto en la magnitud y dirección del error?

Sí; más precisión hace que la magnitud del error sea menor, pero puede cambiar si un cálculo genera una pérdida o una ganancia debido al error. Por ejemplo:

b = 4.00000 / 7.00000;

B sería 0.57143, que se redondea desde el valor verdadero de 0.571428571... Si hubiéramos ido a ocho lugares que serían 0.57142857, que tiene mucho, mucho más pequeño magnitud del error pero en la dirección opuesta; se 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 entre sí o se cancelan entre sí. 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 mayor precisión porque en el cálculo de menor precisión tienes suerte y los errores están en diferentes direcciones.

Esperaríamos que hacer un cálculo con mayor precisión siempre dé 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 flotadores da la respuesta " correcta "pero un cálculo en dobles gives que tienen el doble de precisión gives da la respuesta" incorrecta", ¿correcta?

Sí, esto es exactamente lo que está sucediendo en sus ejemplos, excepto que en lugar de cinco dígitos de precisión decimal tenemos un cierto número de dígitos de precisión binaria. Así como un tercio no se puede representar con precisión en cinco number o cualquier número finito.de dígitos decimales, 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 hacia arriba, 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 entonces, si float y double usan dígitos binarios?

Si necesita una matemática decimal precisa, use el tipo decimal; utiliza fracciones decimales, no binarias fracción. El precio que pagas 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 van a representar con precisión. Cualquier fracción que sea en realidad una fracción decimal sin embargo se representará con cero error, hasta aproximadamente 29 dígitos significativos.

OK, acepto que todos los esquemas de coma flotante introducen inexactitudes debido a errores de representación, y que esas inexactitudes a veces pueden acumularse o se cancelan entre sí en función del número de bits de precisión utilizados en el cálculo. ¿Tenemos al menos la garantía de que esas inexactitudes serán consistentes?

No, usted no tiene tal garantía para flotadores o dobles. Tanto el compilador como el tiempo de ejecución pueden realizar cálculos en coma flotante con una precisión superior a la requerida por la especificación. En particular, el compilador y el tiempo de ejecución pueden hacer una sola precisión (32 bit) aritmética en 64 bits o 80 bits o 128 bits o cualquier bit mayor que 32 que les gusta.

El compilador y el tiempo de ejecución están autorizados a hacerlo sin embargo se sienten así en el momento. No necesitan ser consistentes de máquina a máquina, de carrera a carrera, y así sucesivamente. Dado que esto solo puede hacer cálculos 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 comportan previsiblemente, pero una característica sin embargo.

¿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í.

¿Qué hay de comparar los resultados de 0.1 + 0.2 == 0.3 con (0.1 + 0.2).Equals(0.3)?

Dado que el primero es calculado por el compilador y el segundo es calculado por el tiempo de ejecución, y acabo de decir que se les permite utilice arbitrariamente más precisión de la requerida por la especificación a su antojo, sí, esos pueden dar resultados diferentes. Tal vez uno de ellos elige hacer el cálculo solo con precisión de 64 bits, mientras que el otro elige precisión de 80 bits o 128 bits para parte o todo el cálculo y obtiene una respuesta diferente.

Así que espera un minuto aquí. Estás diciendo no solo que 0.1 + 0.2 == 0.3 puede ser diferente de (0.1 + 0.2).Equals(0.3). Usted está diciendo que 0.1 + 0.2 == 0.3 puede ser calculado para ser verdadero o falso en su totalidad en el capricho del compilador. Podría producir true los martes y false los jueves, podría producir true en una máquina y false en otra, podría producir true y false si la expresión apareciera dos veces en el mismo programa. Esta expresión puede tener cualquier valor por cualquier razón; se permite que el compilador sea completamente no confiable aquí.

Correcto.

La forma en que esto se informa generalmente al equipo del compilador de C# es que alguien tiene algunos expresión que produce true cuando compilan en debug y false cuando compilan en modo release. Esa es la situación más común en la que esto surge porque la generación de código de depuración y liberación cambia los esquemas de asignación de registros. Pero al compilador se le permite hacer lo que quiera con esta expresión, siempre y cuando elija verdadero o falso. (No puede, por ejemplo, producir un error en tiempo de compilación.)

Esto es una locura.

Correcto.

¿A quién debo culpar por este desastre?

Yo no, eso es seguro.

Intel decidió hacer un chip matemático de coma flotante en el que era mucho, mucho más caro hacer resultados consistentes. Las pequeñas opciones en el compilador sobre qué operaciones registrar frente a qué operaciones mantener en la pila pueden agregar grandes diferencias en los resultados.

¿Cómo puedo asegurar resultados consistentes?

Usa el tipo decimal, como dije antes. O hacer todo sus matemáticas en números enteros.

Tengo que usar dobles o flotadores; ¿puedo hacer algo para fomentar resultados consistentes?

Sí. Si almacena cualquier resultado en cualquier campo estático, cualquier campo de instancia de una claseo 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 está expresamente no hecha para tiendas a locales o parámetros formales.) También si lo haces un tiempo de ejecución fundido a (float) o (double) en una expresión que ya es de ese tipo, a continuación, el compilador emitirá un código especial que obliga a que el resultado de truncar como si hubiera sido asignado a un campo o elemento de la matriz. (Los Casts que se ejecutan en tiempo de compilación, es decir, los casts en expresiones constantes, no están garantizados para hacerlo.)

Para aclarar este último punto: ¿la especificación del lenguaje C# hace esas garantías?

No. El runtime garantiza que se almacena en un truncado de matriz o campo. La especificación de C# no garantiza que un cast de identidad se trunca, pero la implementación de Microsoft tiene pruebas de regresión que aseguran que cada nueva versión del compilador tenga este comportamiento.

Todo lo que la especificación de lenguaje tiene que decir sobre el tema es que las operaciones de coma flotante se pueden realizar con mayor precisión a discreción de la implementación.

 130
Author: Eric Lippert,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-02-28 15:38:01

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 un lof de pregunta en señalar ese tema como (Diferencia entre decimal, flotante y doble en. NET? y Tratar con errores de coma flotante en. NET) pero le sugiero que lea el artículo genial llamado;{[15]]}

What Every Computer Scientist Should Know About Floating-Point Arithmetic

Bueno, lo que leppie dijo es más lógico. Real la situación está aquí, depende totalmente de compiler / computer o cpu.

Basado en leppie código, este código funciona en mi Visual Studio 2010 y Linqpad, como resultado True/False, pero cuando me lo probé ideone.com, el resultado será True/True

Compruebe la DEMO.

Tip : 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.

introduzca la descripción de la imagen aquí

 8
Author: Soner Gönül,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 12:02:11

Como se dijo en los comentarios, esto se debe a que el compilador hace una propagación constante y realiza el cálculo con una precisión más alta (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 es emit como false en la IL, por lo tanto el compilador hizo el cálculo en tiempo de compilación.

Para confirmar la optimización constante del compilador de plegado/propagación

  const float f1 = .1f + .2f;
  const float f2 = .3f;
  Console.WriteLine(f1 == f2); // prints false
 5
Author: leppie,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-02-27 17:21:57

FWIW después de pasar la 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);

Así que el problema es en realidad con esta línea

0,1 f + 0,2 f = = 0,3 f

Que, como se ha indicado, es probablemente específico del compilador/pc

La mayoría de la gente está saltando a esta pregunta desde un ángulo equivocado Creo que hasta ahora

ACTUALIZACIÓN:

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)));
}
 2
Author: Valentin Kuzub,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-02-27 17:10:42

== se trata de comparar valores exactos de flotadores.

Equals es un método booleano que puede devolver true o false. La implementación específica puede variar.

 0
Author: njzk2,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-02-27 17:32:49