如何在.NET中将浮点数强制转换为确定性的?

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)转换。
这表明,只需为每个求值为浮点数的表达式和子表达式插入显式转换,就可以实现浮点确定性。可以编写一个包装类型来自动化此任务。这将是一个简单而理想的解决方案!
然而,其他评论表明情况并非如此简单。Eric Lippert最近表示(重点是我的):
引用: 在某个版本的运行时中,显式强制转换为float会产生与不这样做不同的结果。当您显式转换为float时,C#编译器向运行时提供提示,以表示“拿走这个东西。”如果你恰好正在使用这种优化技术,那么你需要退出额外高精度模式。这个“提示”对运行时来说是什么意思呢?C#规范是否规定了显式转换为float会在IL中插入conv.r4指令?CLR规范是否规定了conv.r4指令会导致值缩小到其本机大小?只有两者都成立,我们才能依靠显式转换提供浮点数的“可预测性”,如David Notario所解释的那样。最后,即使我们确实可以强制将所有中间结果转换为类型的本机大小,这是否足以保证在不同计算机上的可复现性,或者是否存在其他因素,如FPU/SSE运行时设置?
2个回答

28

"提示"运行时是什么意思?

正如你所猜测的那样,编译器会跟踪源代码中是否实际存在将类型转换为 double 或 float 的情况,如果存在,则总是插入相应的 conv 操作码。

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

没有,但我向您保证,在编译器测试用例中有单元测试确保它会这样做。尽管规范没有要求这样做,但您可以依赖这种行为。

规范唯一的注释是任何浮点操作都可以在比所需精度更高的精度下执行,这可能会使您的结果出乎意料地更准确。请参见第4.1.6节。

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

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

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

除了强制转换之外,还有哪些操作会将浮点数从高精度模式截断?

有。将值分配给静态字段、实例字段或 double[]float[] 数组的元素时会进行截断。

一致的截断是否足以保证在不同机器上的可重现性?

不是。我鼓励您阅读第12.1.3节,它有很多关于denormals和NaNs的有趣内容。

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

如何确保算术的可重现性?

使用整数。


11
“使用整数”......然而,视频游戏行业在运行诸如即时战略游戏之类的多人模拟时,严重依赖于IEEE754,其中每个客户端必须进行相同的计算并得出相同的结果,以使玩家保持同步。例如,参见http://www.box2d.org/forum/viewtopic.php?p=16662#p16662。这似乎涉及正确配置FPU(_controlfp)和避免使用内置sin(),cos()等实现定义的函数。我会深入研究这个问题,但我相当确定整数(或定点数)运算不是唯一的解决方案。 - Asik
CLR规范的12.1.3节指出,denormal和NaN的行为是实现特定的,但这并不意味着它们一定会导致不可重复性。可能通过限制我想要支持的设备集(比如只有Intel和AMD x86_64 CPU),目标(x86或x64)以及通过_controlfp设置正确的位,denormal和NaN的行为实际上是可以被重现的。如果这意味着我可以针对我想要针对的每个平台进行目标定位,那么我不介意依赖于实现特定的行为。 - Asik
1
使用整数。根据C#规范,对于int.MinValue除以-1int除法是“实现定义的”(除非您使用checked上下文)。引用:“[...]具体实现未指定是抛出System.ArithmeticException(或其子类)还是溢出不报告并且结果值为左操作数的值。”所以它并不是那么“可重现”。 - Jeppe Stig Nielsen
8
我刚碰到这个问题,不得不说(在花了大部分时间解决它之后……)我不明白为什么像 fp:strict 这样的内容不存在。再加上数值类型的通用问题(我在一周左右前曾发泄过),这似乎会使得在 C# 中进行数值分析工作变得更具挑战性。 - Ed S.
值得注意的是,显式转换在这种情况下所做的事情是有保证的,该保证在https://ecma-international.org/publications/files/ECMA-ST/ECMA-334.pdf的第11.1.2节中提供:“在大多数情况下,标识转换在运行时没有任何影响。但是,由于浮点操作可能以高于其类型规定的精度(§9.3.7)执行,因此对其结果进行赋值可能会导致精度损失,并且显式转换保证将精度降低到类型规定的精度。” - fuglede

26
8087浮点数单元芯片设计是英特尔的亿美元错误。这个想法在纸上看起来很好,给它一个8寄存器堆,在扩展精度下存储值,80位。这样你就可以编写计算,其中间值不太可能失去有效数字。
然而,该设备无法进行优化。将FPU堆栈中的值存回内存是昂贵的。因此,将它们保留在FPU内部是一个强有力的优化目标。不可避免的是,如果计算足够深,则只有8个寄存器会要求写回。它也被实现为堆栈,而不是自由可寻址的寄存器,因此需要做一些体操,这可能会产生写回。不可避免地,写回将使80位的值截断为64位,从而丢失精度。
因此,非优化代码与优化代码产生的结果不同。当中间值需要被写回时,对计算进行小的更改可能会产生大的影响。 /fp:strict选项是对此的一种解决方法,它迫使代码生成器发出写回以保持值的一致性,但不可避免地会损失性能。
这是个完全的两难境地。对于x86 Jitter,他们没有尝试解决这个问题。
在设计SSE指令集时,英特尔并没有犯同样的错误。 XMM寄存器是自由可寻址的,不会存储多余的位。如果要获得一致的结果,则使用AnyCPU目标和64位操作系统进行编译是快速解决方案。x64 Jitter使用SSE而不是FPU指令进行浮点数运算。尽管这增加了第三种计算可能产生不同结果的方法。如果计算错误,因为它失去了太多的有效数字,那么它将始终是错误的。这实际上有点像溃疡,但通常只限于程序员的视野范围内。

5
能否在C#编译器中引入类似于“/fp:strict”这样的选项? - Jeppe Stig Nielsen
1
您正在提议添加第四种获取不同结果的方式。 - Hans Passant
4
不,他提出了一种通过在不同的机器上使用相同的代码来获得可重现(虽然不太精确)结果的方法。 - Ed S.
1
8087提供了加载和存储80位数字的指令,是吗?代码为什么不能保持任何“溢出”的寄存器的完整精度呢? - supercat
1
这个回答完全忽略了FPU可以配置为使用双精度而不是扩展精度的事实。如果编译器使用内存写入而不是使用硬件内置的能力来实现严格的双精度,那不是英特尔的错误。 - Ben Voigt
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接