2010-06-21 44 views
7

He notado un comportamiento interesante con el redondeo/truncamiento del flotador por el compilador C#. A saber, cuando un literal flotante está más allá del rango representable garantizado (7 dígitos decimales), entonces a) se expulsa explícitamente un resultado flotante a flotación (una operación semánticamente innecesaria) yb) se almacenan resultados intermedios de cálculo en una variable local. Un ejemplo:Comportamiento extraño del compilador con literales float vs variables float

using System; 

class Program 
{ 
    static void Main() 
    { 
     float f = 2.0499999f; 
     var a = f * 100f; 
     var b = (int) (f * 100f); 
     var c = (int) (float) (f * 100f); 
     var d = (int) a; 
     var e = (int) (float) a; 
     Console.WriteLine(a); 
     Console.WriteLine(b); 
     Console.WriteLine(c); 
     Console.WriteLine(d); 
     Console.WriteLine(e); 
    } 
} 

La salida es:

205 
204 
205 
205 
205 

En la versión de depuración compilados JIT en mi equipo, b se calcula de la siguiente manera:

  var b = (int) (f * 100f); 
0000005a fld   dword ptr [ebp-3Ch] 
0000005d fmul  dword ptr ds:[035E1648h] 
00000063 fstp  qword ptr [ebp-5Ch] 
00000066 movsd  xmm0,mmword ptr [ebp-5Ch] 
0000006b cvttsd2si eax,xmm0 
0000006f mov   dword ptr [ebp-44h],eax 

mientras que D se calcula como

  var d = (int) a; 
00000096 fld   dword ptr [ebp-40h] 
00000099 fstp  qword ptr [ebp-5Ch] 
0000009c movsd  xmm0,mmword ptr [ebp-5Ch] 
000000a1 cvttsd2si eax,xmm0 
000000a5 mov   dword ptr [ebp-4Ch],eax 

Finalmente, mi questi on: ¿por qué la segunda línea de la salida es diferente de la cuarta? ¿Esa fmul adicional hace una gran diferencia? También tenga en cuenta que si el último dígito (ya no representable) del float f se elimina o incluso se reduce, todo "cae en su lugar".

+0

vi contestar a esta pregunta aquí, pero no puede encontrarlo – Andrey

Respuesta

5

Su pregunta se puede simplificar a preguntando por qué estos dos resultados son diferentes:

float f = 2.0499999f; 
var a = f * 100f; 
var b = (int)(f * 100f); 
var d = (int)a; 
Console.WriteLine(b); 
Console.WriteLine(d); 

Si nos fijamos en el código en .NET reflector se puede ver que el código anterior es en realidad compilado como si fuera el código siguiente:

float f = 2.05f; 
float a = f * 100f; 
int b = (int) (f * 100f); 
int d = (int) a; 
Console.WriteLine(b); 
Console.WriteLine(d); 

Los cálculos de punto flotante no siempre se pueden hacer exactamente. El resultado de 2.05 * 100f no es exactamente igual a 205, pero solo un poco menos debido a errores de redondeo. Cuando este resultado intermedio se convierte en un entero, se trunca. Cuando se almacena como un flotador se redondea a la forma representable más cercana. Estos dos métodos de redondeo dan resultados diferentes.


En cuanto a su comentario a mi respuesta cuando se escribe esto:

Console.WriteLine((int) (2.0499999f * 100f)); 
Console.WriteLine((int)(float)(2.0499999f * 100f)); 

Los cálculos se llevan a cabo en su totalidad en el compilador. El código anterior es equivalente a esto:

Console.WriteLine(204); 
Console.WriteLine(205); 
+0

Así que usted dice que la razón es que (int) se realiza por truncamiento y (float) significa redondeo. Si ese es el caso, ¿por qué la salida es diferente para Console.WriteLine ((int) (2.0499999f * 100f)) y Console.WriteLine ((int) (float) (2.0499999f * 100f))? – Alan

+0

@ Alan, verifique mi respuesta. La razón es que el flotador solo puede contener 7 dígitos. log (2^23) = 6.9 – Andrey

+0

@Alan: cuando utiliza constantes codificadas, los cálculos se realizan completamente en el compilador y utilizando las reglas del compilador, no en el tiempo de ejecución de .NET. –

2

Mark tiene razón sobre el compilador. Ahora engañemos al compilador:

float f = (Math.Sin(0.5) < 5) ? 2.0499999f : -1; 
    var a = f * 100f; 
    var b = (int) (f * 100f); 
    var c = (int) (float) (f * 100f); 
    var d = (int) a; 
    var e = (int) (float) a; 
    Console.WriteLine(a); 
    Console.WriteLine(b); 
    Console.WriteLine(c); 
    Console.WriteLine(d); 
    Console.WriteLine(e); 

primera expresión no tiene sentido pero impide la optimización del compilador. El resultado es:

205 
204 
205 
204 
205 

bien, encontré la explicación.

2.0499999f no se puede almacenar como flotante, ya que solo puede contener 7 dígitos basados ​​en 10. y este literal tiene 8 dígitos, por lo que el compilador lo redondeó porque no pudo almacenar. (debe dar una advertencia IMO)

si cambia a 2.049999f se esperarán resultados.

+0

Gracias Andrey, elegí la respuesta de Mark en base al compilador frente a la información de tiempo de ejecución, pero la tuya también es relevante. – Alan

4

En un comentario que le preguntó

¿Son estas reglas diferentes?

Sí. O, más bien, las reglas permiten un comportamiento diferente.

Y si es así, se supone que voy a saber esto, ya sea desde el C# doc referencia del lenguaje o MSDN, o es sólo una discrepancia ocasional entre el compilador y el tiempo de ejecución

Está implícito en el especificación. Las operaciones de coma flotante tienen un cierto nivel mínimo de precisión que debe cumplirse, pero el compilador o el tiempo de ejecución pueden usar más precisión si lo considera apropiado. Eso puede causar cambios grandes y observables cuando realiza operaciones que aumentan pequeños cambios. El redondeo, por ejemplo, puede convertir un cambio extremadamente pequeño en uno extremadamente grande.

Este hecho conduce a las preguntas más frecuentes aquí. Para algunos antecedentes sobre esta situación y otras situaciones que se pueden producir discrepancias similares, ver lo siguiente:

Why does this floating-point calculation give different results on different machines?

C# XNA Visual Studio: Difference between "release" and "debug" modes?

CLR JIT optimizations violates causality?

https://stackoverflow.com/questions/2494724

+1

Eric, muchas gracias. Su último enlace fue especialmente esclarecedor. De hecho, busqué escenarios similares antes de publicar la pregunta, pero aparentemente mi alcance era demasiado estrecho. – Alan

+0

@ Alan: ¡De nada! –