Pac*_*ac0 5 c# floating-point f# non-deterministic language-lawyer
C# 浮点代码的结果可能会导致不同的结果。
这个问题不是0.1 + 0.2 != 0.3关于浮点机器数的原因和固有的不精确性。
这与以下事实有关:具有相同目标架构(例如 x64)的相同C# 代码可能会导致不同的结果,具体取决于所使用的实际机器/处理器。
这个问题与这个问题直接相关:Is浮点数学在C#中是一致的吗?是真的吗?,其中讨论了 C# 问题。
作为参考,C# 规范中的这一段明确说明了该风险:
浮点运算可以以比运算结果类型更高的精度执行。例如,某些硬件体系结构支持具有比双精度类型更大的范围和精度的“扩展”或“长双精度”浮点类型,并使用这种更高精度类型隐式执行所有浮点运算。只有在性能方面付出过高的代价,这样的硬件架构才能以较低的精度执行浮点运算,并且 C# 允许对所有浮点运算使用更高精度的类型,而不是要求实现同时牺牲性能和精度。除了提供更精确的结果之外,这很少有任何可测量的效果
事实上,我们实际上在仅使用 的算法中经历了〜1e-14数量级的差异double,并且我们担心这种差异会传播到使用此结果的其他迭代算法,等等,使得我们的结果对于不同的质量/法律要求不能一致地再现我们在我们的领域(医学成像研究)有。
C# 和 F# 共享相同的 IL 和公共运行时,但是,据我了解,它可能更多地是由编译器驱动的,这对于 F# 和 C# 来说是不同的。
我觉得自己不够精明,无法理解问题的根源是否是两者共有的,或者如果 F# 有希望,我们是否应该跳入 F# 来帮助我们解决这个问题。
C# 语言规范中明确描述了这种不一致问题。我们尚未在 F# 规范中找到等效项(但我们可能没有在正确的位置进行搜索)。
F# 在这方面是否有更多的一致性?
即,如果我们切换到 F#,我们是否能保证在跨架构的浮点计算中获得更一致的结果?
简而言之; C# 和 F# 共享相同的运行时,因此以相同的方式进行浮点数计算,因此当涉及浮点数计算时,您将在 F# 中看到与 C# 中相同的行为。
的问题0.1 + 0.2 != 0.3涉及大多数语言,因为它来自二进制浮点数的 IEEE 标准,其中double有一个例子。在二进制浮点数中不能精确表示0.1、0.2等。这是某些语言支持十六进制浮点文字的原因之一0x1.2p3,它可以精确地表示为二进制浮点数(0x1.2p3等于9十进制数字系统中的顺便说一句)。
许多double内部依赖的软件,如 Microsoft Excel 和 Google Sheet,都会采用各种作弊手段来使数字看起来不错,但通常在数字上并不合理(我不是专家,我只是读了一点 Kahan 的书)。
在 .NET 和许多其他语言中,通常有一种decimal数据类型是十进制浮点数,确保0.1 + 0.2 = 0.3为真。但是,它并不保证 as1/3 + 1/3 = 2/3不能1/3在十进制数字系统中精确表示。由于没有硬件支持,decimal它们往往速度较慢,此外 .NETdecimal不符合 IEEE,这可能是也可能不是问题。
如果您有分数并且有大量可用时钟周期,您可以BigInteger在 F# 中使用“大有理数”。然而,分数很快就会变得非常大,并且不能代表评论中提到的 12 次根,因为根的结果通常是无理数(即不能表示为有理数)。
我想您可以象征性地保留整个计算,并尝试尽可能长时间地保留精确值,然后非常仔细地计算最终数字。可能很难做到正确,而且很可能非常慢。
我读过一些 Kahan 的文章(他共同设计了 8087 和 IEEE 浮点数标准),根据其中一篇论文,我读到了一种实用的方法来检测由于浮点数而导致的舍入误差,即计算三次。
一次采用正常舍入规则,然后采用始终向下舍入,最后采用始终向上舍入。如果最后的数字相当接近,那么计算可能是正确的。
根据卡汉的说法,像“棺材”这样的可爱想法(为每个浮点运算产生一个范围而不是给出最小值/最大值的单个值)只是行不通,因为它们过于悲观,最终会得到无限大的范围。这当然符合我在 C++ boost 库中的经验,它执行此操作,而且速度也非常慢。
因此,当我过去使用 ERP 软件时,我从 Kahan 的文章中了解到,我们应该使用小数来消除类似的“愚蠢”错误,0.1 + 0.2 != 0.3但意识到还有其他错误来源,但消除它们超出了我们的计算范围、存储和能力水平。
希望这可以帮助
附言。这是一个复杂的主题,当我在某个时候更改框架时,我曾经遇到过回归错误。我深入研究后发现错误来自于旧框架中的抖动使用旧式 x86 FPU 指令,而在新的抖动中它依赖于 SSE/AVX 指令。切换到 SSE/AVX 有很多好处,但丢失了一件事,即旧式 FPU 指令内部使用 80 位浮点数,并且只有当浮点数离开 FPU 时,它们才会四舍五入到 64 位,而 SSE/AVX 使用 64 位在内部,这意味着框架之间的结果有所不同。