强制浮点在 .NET 中具有确定性?

Coercing floating-point to be deterministic in .NET?(强制浮点在 .NET 中具有确定性?)

本文介绍了强制浮点在 .NET 中具有确定性?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我已经阅读了很多关于 .NET 中浮点确定性的内容,即确保具有相同输入的相同代码在不同机器上给出相同的结果.由于 .NET 缺少 Java 的 fpstrict 和 MSVC 的 fp:strict 等选项,共识似乎是没有办法解决这个问题使用纯托管代码的问题.C# 游戏 AI Wars 已决定使用 Fixed-point math 代替,但这是一个繁琐的解决方案.

I've been reading a lot about floating-point determinism in .NET, i.e. ensuring that the same code with the same inputs will give the same results across different machines. Since .NET lacks options like Java's fpstrict and MSVC's fp:strict, the consensus seems to be that there is no way around this issue using pure managed code. The C# game AI Wars has settled on using Fixed-point math instead, but this is a cumbersome solution.

主要问题似乎是 CLR 允许中间结果存在于精度高于类型的本机精度的 FPU 寄存器中,从而导致无法预测的高精度结果.CLR 工程师 David Notario 的 MSDN 文章 解释了以下:

The main issue appears to be that the CLR allows intermediate results to live in FPU registers that have higher precision than the type's native precision, leading to impredictably higher precision results. An MSDN article by CLR engineer David Notario explains the following:

请注意,在当前规范下,它仍然是一种语言选择可预测性".该语言可以插入 conv.r4 或 conv.r8每次 FP 操作后的指令以获得可预测"的行为.显然,这真的很昂贵,而且不同的语言都有不同的妥协.例如,如果您愿意,C# 什么都不做缩小,您将不得不手动插入 (float) 和 (double) casts.

Note that with current spec, it’s still a language choice to give ‘predictability’. The language may insert conv.r4 or conv.r8 instructions after every FP operation to get a ‘predictable’ behavior. Obviously, this is really expensive, and different languages have different compromises. C#, for example, does nothing, if you want narrowing, you will have to insert (float) and (double) casts by hand.

这表明可以通过为每个计算结果为浮点数的表达式和子表达式插入显式强制转换来实现浮点确定性.有人可能会围绕 float 编写一个包装器类型来自动执行此任务.这将是一个简单而理想的解决方案!

This suggests that one may achieve floating-point determinism simply by inserting explicit casts for every expression and sub-expression that evaluates to float. One might write a wrapper type around float to automate this task. This would be a simple and ideal solution!

但其他评论表明它并不那么简单.Eric Lippert 最近声明(强调我的):

Other comments however suggest that it isn't so simple. Eric Lippert recently stated (emphasis mine):

在某些版本的运行时,显式转换为浮点数会给出一个与不这样做的结果不同.当您显式转换为浮动时,C# 编译器向运行时提示说拿这个东西如果您碰巧使用它,则退出超高精度模式优化".

in some version of the runtime, casting to float explicitly gives a different result than not doing so. When you explicitly cast to float, the C# compiler gives a hint to the runtime to say "take this thing out of extra high precision mode if you happen to be using this optimization".

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

Just what is this "hint" to the runtime? Does the C# spec stipulate that an explicit cast to float causes the insertion of a conv.r4 in the IL? Does the CLR spec stipulate that a conv.r4 instruction causes a value to be narrowed down to its native size? Only if both of these are true can we rely on explicit casts to provide floating point "predictability" as explained by David Notario.

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

Finally, even if we can indeed coerce all intermediate results to the type's native size, is this enough to guarantee reproducibility across machines, or are there other factors like FPU/SSE run-time settings?

推荐答案

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

The 8087 Floating Point Unit chip design was Intel's billion dollar mistake. The idea looks good on paper, give it an 8 register stack that stores values in extended precision, 80 bits. So that you can write calculations whose intermediate values are less likely to lose significant digits.

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

The beast is however impossible to optimize for. Storing a value from the FPU stack back to memory is expensive. So keeping them inside the FPU is a strong optimization goal. Inevitable, having only 8 registers is going to require a write-back if the calculation is deep enough. It is also implemented as a stack, not freely addressable registers so that requires gymnastics as well that may produce a write-back. Inevitably a write back will truncate the value from 80-bits back to 64-bits, losing precision.

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

So consequences are that non-optimized code does not produce the same result as optimized code. And small changes to the calculation can have big effects on the result when an intermediate value ends up needing to be written back. The /fp:strict option is a hack around that, it forces the code generator to emit a write-back to keep the values consistent, but with the inevitable and considerable loss of perf.

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

This is a complete rock and a hard place. For the x86 jitter they just didn't try to address the problem.

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

Intel didn't make the same mistake when they designed the SSE instruction set. The XMM registers are freely addressable and don't store extra bits. If you want consistent results then compiling with the AnyCPU target, and a 64-bit operating system, is the quick solution. The x64 jitter uses SSE instead of FPU instructions for floating point math. Albeit that this added a third way that a calculation can produce a different result. If the calculation is wrong because it loses too many significant digits then it will be consistently wrong. Which is a bit of a bromide, really, but typically only as far as a programmer looks.

这篇关于强制浮点在 .NET 中具有确定性?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持编程学习网!

本文标题为:强制浮点在 .NET 中具有确定性?

基础教程推荐