为什么在由parantheses分隔和由语句分隔时,C#中的浮点精度会有所不同?

时间:2010-03-22 09:52:21

标签: c# floating-point precision

我知道浮点精度在常规情况下是如何工作的,但我偶然发现了我的C#代码中的奇怪情况。

为什么result1和result2在这里不是完全相同的浮点值?


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

float result1 = (A*B)*dt;

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

this page我认为浮动算术是左关联的,这意味着值是以从左到右的方式计算和计算的。

完整的源代码涉及XNA的Quaternions。我不认为我的常量是什么以及VectorHelper.AddPitchRollYaw()的作用是什么。如果我以相同的方式计算三角形俯仰/滚转/偏航角度,测试通过就好了,但是由于代码低于它而没有通过:


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");
        }
    }

当然,错误很小,只有在经过大量迭代后才出现,但它给我带来了很多好处。

2 个答案:

答案 0 :(得分:9)

Henk完全正确。只是为此添加一点。

这里发生的事情是,如果编译器生成的代码将浮点运算保持在“芯片上”,那么它们可以以更高的精度完成。如果编译器生成的代码每隔一段时间就会将结果移回堆栈,那么每次执行此操作时,额外的精度都会丢失。

编译器是否选择生成更高精度的代码取决于各种未指定的细节:无论是编译调试还是零售,是否在调试器中运行,浮点数是变量还是常量,特定机器具有什么样的芯片架构,等等。

基本上,你保证32位精度或更好,但你永远无法预测你是否会比32位精度更好。因此,您需要不依赖于精确的32位精度,因为这不是我们给您的保证。有时我们会做得更好,有时候不会做得更好,如果你有时会免费获得更好的结果,不要抱怨它。

Henk说他找不到这方面的参考。它是C#规范的4.1.6节,其中规定:

  

浮点运算可能是   执行精度高于   操作的结果类型。对于   例如,一些硬件架构   支持“扩展”或“长双”   具有更大范围的浮点型   和双精度相比,   并隐含地执行所有   使用它的浮点运算   精度更高的类型。只在   性能成本过高就可以了   硬件架构   用。执行浮点运算   精度较低,而不是   要求实施放弃   性能和精度,C#   允许更高精度的类型   用于所有浮点数   操作。除了提供更多   精确的结果,这很少有   可衡量的影响。

至于你应该做什么:首先,总是使用双打。没有任何理由使用浮点数进行算术运算。如果需要,可以使用浮动存储;如果你有一百万个并且想要使用四百万个字节而不是八百万个字节,这对于浮点数是合理的用法。但是在运行时花费成本,因为芯片经过优化可以进行64位数学运算,而不是32位数学运算。

其次,不要依赖浮点结果的准确性或可重现性。条件的微小变化可能导致结果的微小变化。

答案 1 :(得分:7)

我找不到支持这个的参考,但我认为这是由于以下原因:

  • 浮动操作以硬件中可用的精度计算,这意味着它们可以比float更精确地完成。
  • 对中间result2变量的赋值强制舍入回到float精度,但rsult1的单个表达式在向下舍入之前完全以原始精度计算。

在旁注中,使用==测试float或double总是很危险的。 Microsoft单元测试提供am Assert.AreEqual(float expected, float actual,float delta),您可以使用合适的delta来解决此问题。