2010-03-22 7 views
5

Soy consciente de cómo funciona la precisión de punto flotante en los casos normales, pero tropecé con una situación extraña en mi código C#.¿Por qué difiere la precisión de coma flotante en C# cuando está separada por parantheses y cuando está separada por declaraciones?

¿Por qué no result1 y result2 el mismo valor de coma flotante aquí?


const float A; // Arbitrary value 
const float B; // Arbitrary value 

float result1 = (A*B)*dt; 

float result2 = (A*B); 
result2 *= dt; 

De this page que pensé flotar aritmética se asociativo por la izquierda y que esto significa valores se evalúan y se calculan de una manera de izquierda a derecha.

El código fuente completo implica XNA's Quaternions. No creo que sea relevante cuáles son mis constantes y qué hace VectorHelper.AddPitchRollYaw(). La prueba pasa bien si puedo calcular el terreno de juego delta/rollo/guiñada ángulos de la misma manera, pero a medida que el código está debajo de ella no pasa:


X 
    Expected: 0.275153548f 
    But was: 0.275153786f 

[TestFixture] 
    internal class QuaternionPrecisionTest 
    { 
     [Test] 
     public void Test() 
     { 
      JoystickInput input; 
      input.Pitch = 0.312312432f; 
      input.Roll = 0.512312432f; 
      input.Yaw = 0.912312432f; 
      const float dt = 0.017001f; 

      float pitchRate = input.Pitch * PhysicsConstants.MaxPitchRate; 
      float rollRate = input.Roll * PhysicsConstants.MaxRollRate; 
      float yawRate = input.Yaw * PhysicsConstants.MaxYawRate; 

      Quaternion orient1 = Quaternion.Identity; 
      Quaternion orient2 = Quaternion.Identity; 

      for (int i = 0; i < 10000; i++) 
      { 
       float deltaPitch = 
         (input.Pitch * PhysicsConstants.MaxPitchRate) * dt; 
       float deltaRoll = 
         (input.Roll * PhysicsConstants.MaxRollRate) * dt; 
       float deltaYaw = 
         (input.Yaw * PhysicsConstants.MaxYawRate) * dt; 

       // Add deltas of pitch, roll and yaw to the rotation matrix 
       orient1 = VectorHelper.AddPitchRollYaw(
           orient1, deltaPitch, deltaRoll, deltaYaw); 

       deltaPitch = pitchRate * dt; 
       deltaRoll = rollRate * dt; 
       deltaYaw = yawRate * dt; 
       orient2 = VectorHelper.AddPitchRollYaw(
           orient2, deltaPitch, deltaRoll, deltaYaw); 
      } 

      Assert.AreEqual(orient1.X, orient2.X, "X"); 
      Assert.AreEqual(orient1.Y, orient2.Y, "Y"); 
      Assert.AreEqual(orient1.Z, orient2.Z, "Z"); 
      Assert.AreEqual(orient1.W, orient2.W, "W"); 
     } 
    } 

Por supuesto, el error es pequeño y sólo se presenta después de una gran cantidad de iteraciones, pero me ha causado algunos dolores de cabeza.

Respuesta

6

no pude encontrar una referencia a respaldar esto, pero creo que se debe a lo siguiente:

  • operaciones de flotación se calculan en la precisión disponible en el hardware, lo que significa que se puede hacer con una mayor precisión que la de float.
  • la asignación a la variable variable result2 obliga al redondeo a la precisión de flotación, pero la única expresión para rsult1 se calcula completamente en precisión nativa antes de redondearse hacia abajo.

En una nota lateral, probar float o doble con == siempre es peligroso. La prueba de unidad de Microsoft proporciona am Assert.AreEqual(float expected, float actual,float delta) donde resolvería este problema con un delta adecuado (basado en float.Epsilon).

+0

Interesante, un amigo sugirió la idea de que podría haber una instrucción de hardware que toma dos multiplicaciones de flotantes directamente para mejorar la precisión y que al dividir en dos instrucciones, evita que el compilador use esta instrucción especial. – angularsen

+1

@Andreas, AFAIK no hay instrucciones x86 para hacer eso. –

+1

+1. Añadiría que la precisión nativa de los procesadores x86 es 80 bits doble, la precisión nativa de x64 es 64 bits doble. Obtiene los resultados más precisos al permitir que el compilador mantenga cálculos parciales en los registros. Si se le dice al compilador que convierta los productos parciales de nuevo en flotantes, causará pérdida de precisión. –

9

Henk está exactamente en lo cierto. Solo para agregar un poco a eso.

Lo que está sucediendo aquí es que si el compilador genera código que mantiene las operaciones de punto flotante "en el chip", entonces se pueden hacer con mayor precisión. Si el compilador genera código que mueve los resultados de vuelta a la pila de vez en cuando, cada vez que lo hacen, la precesión extra se pierde.

Si el compilador elige generar el código de mayor precisión o no depende de todo tipo de detalles no especificados: si compiló depuración o venta, si está ejecutando en un depurador o no, si los flotantes están en variables o constantes , qué arquitectura de chip tiene la máquina en particular, y así sucesivamente.

Básicamente, tiene una precisión de 32 bits O MEJOR, pero NUNCA puede predecir si obtendrá una precisión superior a 32 bit o no. Por lo tanto, se requiere que NO confíe en tener exactamente una precisión de 32 bits, porque esa no es una garantía que le demos. A veces vamos a hacerlo mejor, y a veces no, y si a veces obtienes mejores resultados gratis, no te quejes.

Henk dijo que no pudo encontrar una referencia sobre esto. Es la sección 4.1.6 de la # especificación C, que establece:

operaciones de coma flotante pueden ser realizado con mayor precisión que el tipo de resultado de la operación. Para ejemplo, algunas arquitecturas de hardware apoyan una o escribe de punto flotante “extendida” “de largo doble” con una mayor gama y precisión que el tipo doble, e implícitamente realizar todas las operaciones de punto flotante utilizando este tipo superior de precisión . Sólo al coste excesivo en el rendimiento se puede hacer este tipo de arquitecturas hardware para realizar operaciones de punto flotante con menos precisión, y en lugar de requiere una implementación que renunciar a tanto el rendimiento y la precisión, C# permite un mayor tipo de precisión que deben utilizado para todas las operaciones de punto flotante . Además de entregar más resultados precisos de , esto raramente tiene ningún efecto mensurable .

En cuanto a lo que debe hacer: Primero, siempre use dobles. No hay ninguna razón para usar flotadores para la aritmética. Use flotadores para el almacenamiento si lo desea; si tiene un millón de ellos y desea utilizar cuatro millones de bytes en lugar de ocho millones de bytes, ese es un uso razonable para los flotadores. Pero flotantes le COST en tiempo de ejecución porque el chip está optimizado para hacer cálculos matemáticos de 64 bits, no matemáticos de 32 bits.

En segundo lugar, no confíe en que los resultados de punto flotante sean exactos o reproducibles. Pequeños cambios en las condiciones pueden causar pequeños cambios en los resultados.

+0

Seguimiento muy bueno y exhaustivo. Entonces entiendo ahora que la aritmética de punto flotante generalmente no es determinista, pero ¿puedo confiar en el mismo cálculo exacto (y la misma entrada) para ser "determinista" en mi programa? Es decir, ¿es posible/probable que la precisión cambie entre cálculos? La razón por la que estoy usando float en primer lugar es porque estoy usando el framework XNA Game Studio, que es un territorio flotante. Probablemente debido a que Xbox 360 y Zune usan el mismo marco. – angularsen

+2

Re: flotadores en XBOX: OK, eso tiene sentido. No me di cuenta de que sus bibliotecas solo usaban carrozas. Una elección extraña. Re: ¿puede la precisión de un cálculo específico cambiar entre invocaciones? En teoría, sí; el jitter puede decir "hey, en base a los datos que he recopilado de las ejecuciones anteriores, veo que puedo generar más velocidad y precisión si regenero el código de esta manera ..." En la práctica, el jitter en realidad no lo hace ese. (Que yo sepa, no soy un experto en el jitter). –

+0

IMO El comentario de Eric sobre el uso del flotador debe estar en cada libro de texto que cubra C# (y más generalmente todos los otros idiomas relevantes). – Ben

Cuestiones relacionadas