在.NET中强制浮点是确定性的吗?

时间:2013-02-13 22:15:12

标签: c# .net floating-point ieee-754

我一直在阅读很多关于.NET中浮点确定性的内容,即确保具有相同输入的相同代码将在不同的机器上提供相同的结果。由于.NET缺少像Java的fpstrict和MSVC的fp:strict,the consensus seems to be这样的选项,因此使用纯托管代码无法绕过这个问题。 C#游戏AI Wars已决定使用Fixed-point math,但这是一个麻烦的解决方案。

主要问题似乎是CLR允许中间结果存在于FPU寄存器中,这些寄存器具有比类型的原始精度更高的精度,从而导致不可预测的更高精度结果。 MSDN article by CLR engineer David Notario解释了以下内容:

  

请注意,根据当前规范,它仍然是一种语言选择   “可预测性”。 语言可能会插入conv.r4或conv.r8   每次FP操作后的指令都可以获得“可预测的”行为。   显然,这是非常昂贵的,不同的语言   不同的妥协。例如,如果你愿意,C#什么都不做   缩小,您将不得不手动插入(浮动)和(双)。

这表明只需通过为每个表达式和计算为float的子表达式插入显式强制转换,就可以实现浮点确定性。有人可能会在float周围编写一个包装器类型来自动执行此任务。这将是一个简单而理想的解决方案!

然而,其他评论表明并非如此简单。 Eric Lippert recently stated(强调我的):

  

在某些版本的运行时中,show to float显式给出了一个   结果不同于不这样做。当您显式转换为float时,   C#编译器给运行时提示说“拿这个东西   超出高精度模式,如果你碰巧使用它   优化”。

这对运行时的“提示”是什么? C#规范是否规定显式转换为float会导致在IL中插入conv.r4? CLR规范是否规定conv.r4指令会使值缩小到其原始大小?只有当这两者都成立时,我们才能依靠显式强制转换来提供浮点“可预测性”,如David Notario所解释的那样。

最后,即使我们确实能够将所有中间结果强制转换为类型的原生大小,这是否足以保证机器之间的可重复性,还是存在其他因素,如FPU / SSE运行时设置?

2 个答案:

答案 0 :(得分:26)

  

这对运行时的“提示”是什么?

正如您猜想的那样,编译器会跟踪源代码中是否实际存在转换为double或float,如果是,则它总是插入适当的转换操作码。

  

C#规范是否规定显式转换为float会导致在IL中插入conv.r4?

不,但我向您保证,编译器测试用例中有单元测试可以确保它完成。虽然规范不要求它,但你可以依赖这种行为。

规范的唯一注释是,任何浮点运算都可以以更高精度完成,而不是运行时的奇思妙想,这可以使您的结果意外地更准确。见4.1.6。

  

CLR规范是否规定conv.r4指令会将值缩小到原始大小?

是的,在分区I,第12.1.3节中,我注意到您可以自己查找,而不是要求互联网为您执行此操作。这些规范在网上免费提供。

你没有提出但可能应该提出的问题:

  

除了强制转换之外是否有任何操作可以将浮点数截断为高精度模式?

是。分配给double[]float[]数组的静态字段,实例字段或元素会截断。

  

是否一致截断足以保证机器之间的重复性?

没有。我鼓励你阅读第12.1.3节,其中有关非正规和NaN的主题有很多有趣的说法。

最后,你没有提出的另一个问题,但可能应该有:

  

我如何保证可重复的算术?

使用整数。

答案 1 :(得分:24)

8087浮点单元芯片设计是英特尔十亿美元的错误。这个想法在纸上看起来不错,给它一个8寄存器堆栈,以80位扩展精度存储值。这样你就可以编写中间值不太可能丢失有效数字的计算。

然而,野兽无法优化。将FPU堆栈中的值存储回内存非常昂贵。因此,将它们保留在FPU中是一个强大的优化目标。不可避免的是,如果计算足够深,只有8个寄存器就需要回写。它也被实现为堆栈,而不是可自由寻址的寄存器,因此需要体操也可能产生回写。回写不可避免地会将截断值从80位反转为64位,从而失去精度。

因此,非优化代码不会产生与优化代码相同的结果。当中间值最终需要写回时,计算的微小变化会对结果产生很大影响。 / fp:strict选项是一个黑客攻击,它强制代码生成器发出回写以保持值一致,但是不可避免且相当大的性能损失。

这是一个完整的岩石和一个艰难的地方。对于x86抖动,他们只是没有尝试解决这个问题。

英特尔在设计SSE指令集时没有犯同样的错误。 XMM寄存器可自由寻址,不存储额外的位。如果您想要一致的结果,那么使用AnyCPU目标和64位操作系统进行编译是快速解决方案。 x64抖动使用SSE而不是FPU指令进行浮点数学运算。虽然这增加了计算可以产生不同结果的第三种方式。如果计算错误,因为它丢失了太多有效数字,那么它将一直是错误的。实际上,这有点像溴化物,但通常只有程序员看起来。