2009-03-30 19 views
7

Consideremos el siguiente código C#:¿Por qué afecta el orden del redondeo al agregar múltiples dobles en C#

double result1 = 1.0 + 1.1 + 1.2; 
double result2 = 1.2 + 1.0 + 1.1; 

if (result1 == result2) 
{ 
    ... 
} 

resultado1 debe ser siempre igual result2 ¿verdad? El caso es que no. result1 es 3.3 y result2 es 3.3000000000000003. La única diferencia es el orden de las constantes.

Sé que los dobles se implementan de tal forma que pueden ocurrir problemas de redondeo. Soy consciente de que puedo usar decimales si necesito precisión absoluta. O que puedo usar Math.Round() en mi sentencia if. Solo soy un nerd que quiere entender lo que hace el compilador de C#. ¿Alguien me puede decir?

Editar:

Gracias a todos los que hasta ahora sugieren leer sobre la aritmética de coma flotante y/o hablado sobre la inexactitud inherente de cómo la CPU maneja dobles. Pero siento que el impulso principal de mi pregunta aún no ha sido respondido. Cuál es mi culpa por no expresarlo correctamente. Déjame ponerlo de esta manera:

Romper el código anterior, esperaría que las siguientes operaciones para estar sucediendo:

double r1 = 1.1 + 1.2; 
double r2 = 1.0 + r1 
double r3 = 1.0 + 1.1 
double r4 = 1.2 + r3 

Vamos a suponer que cada una de las adiciones anteriores tenían un error de redondeo (e1 numerada. .e4). Entonces, r1 contiene el error de redondeo e1, r2 incluye los errores de redondeo e1 + e2, r3 contiene e3 y r4 contiene e3 + e4.

Ahora, no sé exactamente cómo ocurren los errores de redondeo pero esperaba que e1 + e2 fuera igual a e3 + e4. Está claro que no, pero eso me parece algo malo. Otra cosa es que cuando ejecuto el código anterior, no obtengo ningún error de redondeo. Eso es lo que me hace pensar que es el compilador C# que está haciendo algo raro en lugar de la CPU.

Sé que estoy pidiendo mucho y tal vez la mejor respuesta que alguien puede dar es ir a hacer un doctorado en diseño de CPU, pero pensé que podría preguntar.

Editar 2

En cuanto a la IL de mi ejemplo de código original, es claro que es el compilador no la CPU que está haciendo esto:

.method private hidebysig static void Main(string[] args) cil managed 
{ 
.entrypoint 
    .maxstack 1 
    .locals init (
     [0] float64 result1, 
     [1] float64 result2) 
    L_0000: nop 
    L_0001: ldc.r8 3.3 
    L_000a: stloc.0 
    L_000b: ldc.r8 3.3000000000000003 
    L_0014: stloc.1 
    L_0015: ret 
} 

El compilador es sumar los números de ¡yo!

Respuesta

10

lo que habría esperado e1 + e2 + e3 a la igualdad de e4.

Eso no es del todo a diferencia esperando

floor(5/3) + floor(2/3 + 1) 

para igualar

floor(5/3 + 2/3) + floor(1) 

excepto que estás multiplicando por 2^53 antes de tomar la palabra.

El uso de 12 bits de precisión y el truncamiento punto flotante con sus valores:

 
1.0   = 1.00000000000 
1.1   = 1.00011001100 
1.2   = 1.00110011001 

1.0 + 1.1  = 10.00011001100 // extended during sum 
r1 = 1.0 + 1.1 = 10.0001100110 // truncated to 12 bit 
r1 + 1.2  = 11.01001100101 // extended during sum 
r2 = r1 + 1.2 = 11.0100110010 // truncated to 12 bit 

1.1 + 1.2  = 10.01001100110 // extended during sum 
r3 = 1.1 + 1.2 = 10.0100110011 // truncated to 12 bit 
r3 + 1.0  = 11.01001100110 // extended during sum 
r4 = r3 + 1.0 = 11.0100110011 // truncated to 12 bit 

tanto, cambiar el orden de las operaciones/truncamientos hace que el error de cambiar, y r4 = r2!. Si agrega 1.1 y 1.2 en este sistema, el último bit se transmite, por lo que no se pierde en el truncamiento. Si agrega 1.0 a 1.1, se pierde el último bit de 1.1 y el resultado no es el mismo.

En un pedido, el redondeo (por truncamiento) elimina un 1 final.

En el otro pedido, el redondeo elimina un tramo 0 en ambas ocasiones.

Uno no es igual a cero; entonces los errores no son lo mismo

Los dobles tienen muchos más bits de precisión, y C# probablemente usa redondeo en lugar de truncamiento, pero es de esperar que este modelo simple muestre que pueden ocurrir diferentes errores con diferentes ordenamientos de los mismos valores.

La diferencia entre fp y matemática es que + es una abreviación para 'agregar y redondear' en lugar de solo agregar.

+0

+1 buen ejemplo concreto – bobince

+0

El ejemplo es bueno, pero ha cambiado el orden de las operaciones en comparación con el código original. El OP hizo lo mismo en la primera edición. –

2

El orden de las operaciones de coma flotante es importante. No responde directamente a su pregunta, pero siempre debe tener cuidado al comparar los números de coma flotante. Es usual incluir una tolerancia:

double epsilon = 0.0000001; 
if (abs(result1 - result2) <= epsilon) 
{ 
    ... 
} 

Esto puede ser de interés: What Every Computer Scientist Should Know About Floating-Point Arithmetic

6

El compilador de C# no está haciendo nada. La CPU es.

si tiene A en un registro de la CPU, y a continuación, añadir B, el resultado almacenado en dicho registro es A + B, aproximarse a la precisión flotante utilizado

Si a continuación, añadir C, el error se suma . Esta adición de error no es una operación transitiva, por lo tanto, la diferencia final.

+0

Pero si A + B produce un error de redondeo y A + C no, ¿no tendría (A + C) + B el mismo error de redondeo que A + B? Me doy cuenta de que los errores de redondeo se suman, me pregunto por qué es importante el orden. – d4nt

+0

no, A + B produce un error e1, y luego A + B + C produce otro error e2, por lo que el error final es e1 + e2. Si A + C no proporciona ningún error, A + C + B proporciona un solo error e3, que no tiene ninguna razón para coincidir con e1 + e2. Debes buscar en Google aritmética de punto flotante para obtener más información. – Brann

+0

Supongo que no me estoy aclarando, permítanme reformular "si A + B hace e1, (A + B) + C hace e2, A + C hace e3 y (A + C) + B hace e4, ¿por qué e1 + e2 no es igual a e3 + e4. Hasta ahora, me has dicho que no, pero no por qué no funciona ". – d4nt

0

En realidad se está utilizando no los mismos valores, porque los resultados intermedios son diferentes:

double result1 = 2.1 + 1.2; 
double result2 = 2.2 + 1.1; 

Debido a que los dobles no pueden representar valores decimales es exactamente lo que obtienen resultados diferentes.

4

Consulte the classic paper (What every computer scientist should know about floating-point arithmetic) sobre el tema. Este tipo de cosas es lo que sucede con la aritmética de coma flotante. Se necesita un informático para decirle que 1/3 + 1/3 + 1/3 is'nt igual a 1 ...

+0

Admito que la aritmética de punto flotante me asusta. Como regla general, me niego a usarlo a menos que deba hacerlo. –

+0

Úselos cuando sea útil: donde las aproximaciones son correctas. Simulación física, mediciones adquiridas externamente, diseño gráfico (para diferentes niveles de aproximación permitida). Dinero, ¡no tanto! No existe una herramienta tan simple que no puedas lastimarte con ella ... (¿Hm, accidentes mortales de pinzas?). –

1

Por qué los errores no son los mismos dependiendo del orden se pueden explicar con un ejemplo diferente.

Digamos que para los números inferiores a 10, puede almacenar todos los números, por lo que puede almacenar 1, 2, 3, y hasta 10, pero después de 10, solo puede almacenar cada segundo número, debido a la pérdida interna de precisión, en otras palabras, sólo puede almacenar 10, 12, 14, etc.

Ahora, con ese ejemplo, verá por qué la siguiente produce resultados diferentes:

1 + 1 + 1 + 10 = 12 (or 14, depending on rounding) 
10 + 1 + 1 + 1 = 10 

El problema con los números de punto flotante es que no se pueden representar con precisión, y el error no siempre es el mismo, por lo que el orden será importante.

Por ejemplo, 3,00000000003 + 3,00000000003 podría terminar siendo 6,00000000005 (aviso no 6 al final), pero 3.00000000003 + 2,99999999997 podría terminar siendo 6,00000000001, y con eso:

step 1: 3.00000000003 + 3.00000000003 = 6.00000000005 
step 2: 6.00000000005 + 2.99999999997 = 9.00000000002 

pero, cambiar el orden :

step 1: 3.00000000003 + 2.99999999997 = 6.00000000001 
step 2: 6.00000000001 + 3.00000000003 = 9.00000000004 

Así que será importante.

Ahora, por supuesto, puede ser afortunado en que los ejemplos de arriba se equilibran entre sí, en que el primero se moverá hacia arriba por .xxx1 y el otro por .xxx1, dándole .xxx3 en ambos, pero hay sin garantía.

Cuestiones relacionadas