我已经阅读了很多有关.NET中浮点确定性的文章,即确保相同的代码和相同的输入将在不同的机器上提供相同的结果。由于.NET缺少Java的fpstrict和MSVC的fp:strict之类的选项,因此共识似乎是,使用纯托管代码无法解决此问题。C#游戏AI Wars已经改用定点数学,但这是一个繁琐的解决方案。
主要问题似乎是CLR允许中间结果存在于FPU寄存器中,该寄存器的精度比该类型的本机精度更高,从而导致不可预测的更高精度结果。CLR工程师David Notario的MSDN文章解释了以下内容:
请注意,在当前规范下,提供“可预测性”仍然是一种语言选择。 该语言可以在每个FP操作之后插入conv.r4或conv.r8指令,以实现“可预测”的行为。 显然,这确实很昂贵,并且不同的语言会有不同的折衷。例如,C#不执行任何操作,如果您想缩小范围,则必须手动插入(float)和(double)强制转换。
这表明,只需为评估为float的每个表达式和子表达式插入显式强制转换即可实现浮点确定性。可能会在float周围编写包装类型以自动执行此任务。这将是一个简单而理想的解决方案!
但是,其他评论表明,它并不是那么简单。埃里克·利珀特(Eric Lippert)最近表示(重点是我):
在某些版本的运行时中,强制转换为float会显着不同。当您显式转换为float时,C#编译器 会提示运行时 说:“如果碰巧正在使用此优化,则将其退出超高精度模式”。
运行时的“提示”到底是什么?C#规范是否规定显式强制转换为float会导致在IL中插入conv.r4?CLR规范是否规定conv.r4指令会使值缩小到其原始大小?只有这两个都是正确的,我们才能依靠显式强制转换来提供浮点“可预测性”,正如David Notario所解释的那样。
最后,即使我们确实可以将所有中间结果强制为该类型的原始大小,这是否足以保证跨机器的可重复性,还是存在其他因素,例如FPU / SSE运行时设置?
8087 Floating Point Unit芯片设计是Intel数十亿美元的错误。这个想法在纸面上看起来不错,给它一个8寄存器堆栈,以80位扩展精度存储值。这样就可以编写中间值不太可能丢失有效数字的计算。
但是,野兽是无法优化的。将值从FPU堆栈存储回内存的成本很高。因此,将它们保留在FPU中是一个强大的优化目标。如果计算足够深,那么只有8个寄存器将不可避免地需要回写。它还被实现为堆栈,而不是可自由寻址的寄存器,因此也需要进行体操操作,并且可能产生回写。不可避免地,回写会将值从80位 截断 为64位,从而失去精度。
因此后果是,未优化的代码不会产生与优化的代码相同的结果。当最终需要回写中间值时,对计算的小的更改可能会对结果产生很大的影响。/ fp:strict选项解决了这一问题,它迫使代码生成器发出回写以保持值一致,但不可避免且会造成性能损失。
这是一块完整的岩石,一个艰苦的地方。对于x86抖动,他们只是没有尝试解决问题。
英特尔在设计SSE指令集时没有犯同样的错误。XMM寄存器是可自由寻址的,并且不存储额外的位。如果要获得一致的结果,则可以使用AnyCPU目标和64位操作系统进行编译是快速的解决方案。x64抖动使用SSE而不是FPU指令进行浮点运算。尽管这增加了计算可以产生不同结果的第三种方式。如果由于丢失太多有效数字而导致计算错误,那么它将始终是错误的。的确,这有点像溴化物,但通常仅在程序员看来是这样。