47

我已经阅读了很多关于 .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 最近表示(强调我的):

在运行时的某些版本中,显式转换为浮动会产生与不这样做不同的结果。当您显式转换为浮点数时,C# 编译器会提示运行时说“如果您碰巧使用此优化,请将此事物退出超高精度模式”。

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

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

4

2 回答 2

28

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

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

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

不,但我向您保证,编译器测试用例中有单元测试可以确保它确实如此。尽管规范没有要求,但您可以依赖此行为。

该规范的唯一评论是,任何浮点运算都可能以比运行时所需的精度更高的精度完成,这可以使您的结果出乎意料地更加准确。请参阅第 4.1.6 节。

CLR 规范是否规定 conv.r4 指令会导致将值缩小到其本机大小?

是的,在第 I 部分第 12.1.3 节中,我注意到您可以自己查找而不是要求互联网为您查找。这些规范在网络上是免费的。

一个你没有问但可能应该问的问题:

除了强制转换之外还有其他操作可以将浮点数从高精度模式中截断吗?

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

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

不,我鼓励你阅读第 12.1.3 节,它对非正规数和 NaN 的主题有很多有趣的说法。

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

如何保证可重现的算术?

使用整数。

于 2013-02-13T22:56:39.877 回答
25

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

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

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

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

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

于 2013-02-13T23:34:01.673 回答