我在.NET中读了很多关于浮点决定论的文章,即确保相同的代码具有相同的输入,在不同的机器上得到相同的结果。由于.NET缺乏像Java和MSVC的fp:共识似乎是这样的选项,所以使用纯托管代码是无法解决这个问题的。C#游戏AI战争已经决定使用定点数学代替,但这是一个麻烦的解决方案。
主要问题似乎是CLR允许中间结果驻留在FPU寄存器中,这些寄存器具有比类型的本机精度更高的精度,从而导致不可预测的更高精度的结果。MSDN文章,CLR工程师David Notario解释如下:
请注意,对于当前的规范,提供“可预见性”仍然是一种语言选择。语言可以在每次FP操作后插入conv.r4或conv.r8指令,以获得“可预测”的行为。显然,这是非常昂贵的,不同的语言有不同的妥协。例如,C#什么也不做,如果您想缩小范围,就必须手动插入(浮动)和(双)强制转换。
这表明,可以通过为计算为浮动的每个表达式和子表达式插入显式转换来实现浮点决定论。您可能会在float周围编写一个包装器类型来自动执行此任务。这将是一个简单和理想的解决方案!
然而,其他一些评论表明,事情并不那么简单。埃里克·利珀特最近说 (重点地雷):
在运行时的某些版本中,显式地进行浮点转换会给出与不这样做不同的结果。当您显式转换为浮动时,C#编译器向运行时提供了一个提示:“如果您正在使用此优化,请将其从超高精度模式中删除”。
这对运行时是什么“提示”呢?C#规范是否规定要浮动的显式强制转换导致在IL中插入conv.r4?CLR规范是否规定conv.r4指令会使值缩小到其本机大小?只有当这两者都是真的时,我们才能依赖显式转换来提供浮点“可预测性”,正如David所解释的那样。
最后,即使我们确实可以将所有中间结果强制到该类型的本地大小,这是否足以保证计算机之间的可再现性,或者是否还有其他因素,如FPU/SSE运行时设置?
发布于 2013-02-13 23:34:01
8087浮点单元芯片的设计是英特尔数十亿美元的错误。这个想法在纸上看起来很好,给它一个8寄存器栈,它以扩展的精度存储值,80位。这样,您就可以编写中间值不太可能丢失显着数字的计算。
然而,这只野兽是不可能优化的。将一个值从FPU堆栈存储回内存是很昂贵的。因此,将它们保存在FPU中是一个很强的优化目标。不可避免的是,如果计算足够深入,只有8个寄存器将需要回写。它还被实现为一个堆栈,而不是自由寻址寄存器,因此,这也需要体操,以及可能产生的回写。不可避免的是,写回会将值从80位截断回64位,从而失去精度.
因此,结果是,非优化代码不会产生与优化代码相同的结果。当一个中间值最终需要被写回时,对计算的小改动会对结果产生很大的影响。/fp:strict选项是一个黑客,它强制代码生成器发出一个回放,以保持值一致,但不可避免和相当大的损失的perf。
这是一个完整的岩石和一个艰难的地方。对于x86抖动,他们只是没有试图解决这个问题。
英特尔在设计SSE指令集时没有犯同样的错误。XMM寄存器是可自由寻址的,不存储额外的位。如果您希望得到一致的结果,那么使用AnyCPU目标和64位操作系统编译是快速的解决方案。x64抖动使用SSE代替浮点运算的FPU指令。尽管这增加了计算可以产生不同结果的第三种方式。如果计算是错误的,因为它失去了太多的重要数字,那么它将始终是错误的。实际上,这是一种溴化,但通常只限于程序员所看到的范围。
发布于 2013-02-13 22:56:39
这对运行时是什么“提示”呢?
正如您猜测的那样,编译器跟踪源代码中是否实际存在到double或float的转换,如果是的话,它总是插入适当的conv操作码。
C#规范是否规定要浮动的显式强制转换导致在IL中插入conv.r4?
不是,但我向您保证,编译器测试用例中有单元测试,确保了它的存在。虽然规范没有要求,但您可以依赖于这种行为。
规范的唯一注释是,任何浮点操作都可以在运行时的任意时刻以更高的精度完成,这可以使您的结果出乎意料地更精确。见第4.1.6节。
CLR规范是否规定conv.r4指令会使值缩小到其本机大小?
是的,在Partition,第12.1.3节中,我注意到,你可以查找自己的,而不是要求互联网为你做。这些规范在网上是免费的。
一个你没有问但很可能应该问的问题:
除了铸造,是否有任何操作可以使浮子脱离高精度模式?
是。为double[]
或float[]
数组的静态字段、实例字段或元素分配截断。
一致性截断是否足以保证机器之间的可再现性?
不是的。我鼓励你们阅读第12.1.3节,其中有许多有趣的话要说的主题是非正常人和非土著人。
最后,另一个你没有问但很可能应该问的问题是:
我如何保证可重复的算术?
使用整数。
https://stackoverflow.com/questions/14864238
复制相似问题