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

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

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

const float A; // Arbitrary value const float B; // Arbitrary value float result1 = (A*B)*dt; float result2 = (A*B); result2 *= dt; 

从这个页面我认为浮动算术是左关联的,这意味着值以从左到右的方式进行评估和计算。

完整的源代码涉及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"); } } 

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

我无法找到支持这一点的参考,但我认为这是由于以下原因:

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

另外,使用==测试float或double总是很危险的。 Microsoftunit testing提供了Assert.AreEqual(float expected, float actual,float delta) ,您可以使用合适的delta解决此问题。

亨克是完全正确的。 只是为此添加一点。

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

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

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

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

可以以比操作的结果类型更高的精度执行浮点运算。 例如,某些硬件体系结构支持“扩展”或“长双”浮点类型,其范围和精度高于double类型,并使用此更高精度类型隐式执行所有浮点运算。 只有在性能成本过高的情况下,才能使这种硬件架构以较低的精度执行浮点运算,而不是要求实现失去性能和精度,C#允许更高精度的类型用于所有浮点运算。 除了提供更精确的结果外,这很少有任何可衡量的影响。

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

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