Coercing floating-point to be deterministic in .NET?
我已经读了很多关于.NET中浮点决定论的文章,也就是说,确保具有相同输入的相同代码在不同的机器上产生相同的结果。由于.NET缺少像Java的FP严格和MSVC的FP:严格的选项,所以共识似乎是使用纯托管代码无法绕过这个问题。C游戏人工智能战争已经解决了使用定点数学代替,但这是一个麻烦的解决方案。
主要的问题似乎是,clr允许中间结果存在于fpu寄存器中,这些寄存器的精度高于类型的本机精度,从而导致难以想象的更高精度结果。CLR工程师David Notario的一篇MSDN文章解释了以下内容:
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的表达式和子表达式插入显式类型转换,就可以实现浮点决定论。可以围绕float编写一个包装类型来自动执行此任务。这将是一个简单而理想的解决方案!
然而,其他评论却表明这并不简单。埃里克·利珀特最近说(强调我的):
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指令会导致值缩小到其本机大小?只有当这两个都是真的,我们才能依靠明确的强制转换来提供浮点"可预测性",正如大卫·诺瓦里奥所解释的那样。
最后,即使我们确实可以将所有中间结果强制为类型的本机大小,这是否足以保证跨计算机的可复制性,或者是否存在其他因素,如fpu/sse运行时设置?
Just what is this"hint" to the runtime?
正如您猜想的那样,编译器跟踪源代码中是否存在转换为double或float的过程,如果是这样,它总是插入适当的conv操作码。
Does the C# spec stipulate that an explicit cast to float causes the insertion of a conv.r4 in the IL?
不,但我向您保证编译器测试用例中有一些单元测试可以确保它是这样的。尽管规范没有要求,但是您可以依赖于这种行为。
规范唯一的意见是,任何浮点操作都可以以比运行时所需的更高的精度进行,这可以使您的结果出人意料地更准确。见第4.1.6节。
Does the CLR spec stipulate that a conv.r4 instruction causes a value to be narrowed down to its native size?
是的,在第一部分第12.1.3节,我注意到你可以自己查一下,而不是让互联网帮你查。这些规格在网上是免费的。
一个你没有问但可能应该问的问题:
Is there any operation other than casting that truncates floats out of high precision mode?
对。分配给
Is consistent truncation enough to guarantee reproducibility across machines?
不,我鼓励你阅读第12.1.3节,在非规范化和NAN问题上有很多有趣的内容。
最后,还有一个你没有问但可能应该问的问题:
How can I guarantee reproducible arithmetic?
使用整数。
8087浮点单元芯片的设计是英特尔数十亿美元的错误。这个想法在纸上看起来很好,给它一个8寄存器堆栈,以扩展精度存储值,80位。这样就可以编写中间值不太可能丢失有效数字的计算。
然而,这种野兽是不可能优化的。将值从FPU堆栈存储回内存是昂贵的。因此,将它们保存在FPU中是一个很强的优化目标。不可避免的是,只有8个寄存器需要回写,如果计算足够深的话。它也被实现为一个堆栈,而不是一个可自由寻址的寄存器,因此需要体操,也可能产生回写。不可避免地,写回会将值从80位截断回64位,从而失去精度。
因此,结果是非优化代码不会产生与优化代码相同的结果。当中间值最终需要回写时,对计算结果的小改动会对结果产生很大影响。/fp:strict选项是一个破解方法,它强制代码生成器发出回写以保持值的一致性,但会不可避免地造成相当大的性能损失。
这是一块完整的岩石和一个坚硬的地方。对于x86抖动,他们只是没有尝试解决问题。
英特尔在设计SSE指令集时并没有犯同样的错误。XMM寄存器是可自由寻址的,不存储额外的位。如果您希望得到一致的结果,那么使用anycpu目标和64位操作系统进行编译是快速解决方案。X64抖动使用SSE而不是FPU指令进行浮点运算。尽管这增加了第三种方法,计算可以产生不同的结果。如果计算错误,因为它丢失了太多的有效数字,那么它将始终是错误的。这是一个有点溴化,真的,但通常只有在程序员看来。