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

我一直在阅读很多关于.NET中浮点确定性的知识,即确保具有相同input的相同代码将在不同的机器上得到相同的结果。 由于.NET缺乏诸如Java的fpstrict和MSVC的fp:strict之类的选项, 所以共识似乎是使用纯托pipe代码无法解决此问题。 C#游戏人工智能战争已经决定使用定点math ,但这是一个繁琐的解决scheme。

主要的问题似乎是CLR允许中间结果存在于FPU寄存器中,这些寄存器的精度比types本身的精度要高,从而导致精确的结果难以预测。 CLR工程师David Notario的MSDN文章解释如下:

请注意,在目前的规范下,它仍然是一个提供“可预测性”的语言select。 在每次FP操作之后,语言可以插入conv.r4或conv.r8指令以获得“可预测”的行为。 很明显,这个代价很高,不同的语言有不同的折中。 例如,C#不做任何事,如果你想缩小,你将不得不手动插入(浮动)和(双)转换。

这表明可以简单地通过为每个expression式和子expression式插入明确的强制转换来实现浮点确定。 有人可能会写一个浮动types的包装types来自动完成这个任务。 这将是一个简单而理想的解决scheme!

其他意见却表明这并不是那么简单。 Eric Lippert最近表示 (强调我的):

在运行时的某个版本中,明确地转换为浮动会产生比不这样做的结果。 当你明确强制转换为浮点运算时,C#编译器给运行时提示 “如果碰巧使用了这种优化,就把它从超高精度模式中拿出来”。

这是什么“提示”到运行时? C#规范是否规定明确的强制转换会导致在IL中插入conv.r4? CLR规范是否规定conv.r4指令将价值缩小到原始大小? 只有这两者都是真实的,我们才能依靠明确的演员来提供浮点“可预测性”,正如David Notario所解释的那样。

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

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

正如你猜测的那样,编译器跟踪源代码中是否实际存在一个double或float的转换,如果是的话,它总是插入相应的conv操作码。

C#规范是否规定明确的强制转换会导致在IL中插入conv.r4?

不,但我向你保证,在编译器testing用例中有unit testing确保它能做到。 虽然规范并不要求它,但你可以依赖这种行为。

规范的唯一注释是任何浮点运算都可以以比运行时突发奇想更高的精度完成,并且这可以使您的结果意外地更加准确。 参见4.1.6节。

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

是的,在第一部分第12.1.3节中,我注意到你可以自己抬头看看,而不是要求互联网为你做。 这些规范在networking上是免费的。

你没问的问题可能应该有:

有没有其他的操作,而不是从高精度模式截断浮动的铸造?

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

一致的截断是否足以确保跨机器的可重复性?

不,我鼓励你阅读第12.1.3节,关于非正规化和NaNs这个话题有很多有趣的说法。

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

我怎样才能保证可重复的算术?

使用整数。

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

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

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

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

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