F# 是否在非确定性浮点计算方面受到相同的 C# 警告?

Does F# suffer from same C# caveats on non-deterministic floating point calculation?

C# 浮点代码的结果可能会导致不同的结果。

这个问题不是为什么0.1 + 0.2 != 0.3和浮点机器数固有的不精确性。

这与以下事实有关:相同的 C# 代码,具有相同的目标架构(例如 x64),可能会导致不同的结果 取决于实际使用的机器/处理器。

此问题与此问题直接相关:Is floating-point math consistent in C#? Can it be?,其中讨论了 C# 问题。

作为参考,C# 规范中的 this paragraph 明确说明了该风险:

Floating-point operations may be performed with higher precision than the result type of the operation. For example, some hardware architectures support an "extended" or "long double" floating-point type with greater range and precision than the double type, and implicitly perform all floating-point operations using this higher precision type. Only at excessive cost in performance can such hardware architectures be made to perform floating-point operations with less precision, and rather than require an implementation to forfeit both performance and precision, C# allows a higher precision type to be used for all floating-point operations. Other than delivering more precise results, this rarely has any measurable effects

事实上,我们实际上在仅使用 double 的算法中经历了 ~1e-14 数量级的差异,我们担心这种差异会传播到使用该结果的其他迭代算法,并且如此等等,使得我们的结果无法始终如一地重现,因为我们在我们的领域(医学成像研究)有不同的质量/法律要求。

C# 和 F# 共享相同的 IL 和公共运行时,但是,据我了解,它可能更多是由编译器驱动的东西,这对于 F# 和 C# 是不同的。

我觉得我不够聪明,无法理解问题的根源是否对两者都是共同的,或者如果 F# 有希望,我们是否应该跳入 F# 来帮助我们解决这个问题。

TL;DR

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 为真。但是,它不能保证 1/3 + 1/3 = 2/31/3 不能在十进制数字系统中精确表示。由于没有支持 decimal 的硬件,它们往往会变慢,此外 .NET decimal 不符合 IEEE,这可能是也可能不是问题。

如果您有小数并且有很多时钟周期可用,您可以在 F# 中使用 BigInteger 实现 "big rational"。然而,分数很快变得非常大,它不能代表评论中提到的 12 次根,因为根的结果通常是无理数(即不能表示为有理数)。

我想你可以象征性地保留整个计算并尝试尽可能长时间地保留精确值,然后非常仔细地计算最终数字。可能很难做到正确,而且很可能很慢。

我读过一点 Kahan(他共同设计了 8087 和浮点数的 IEEE 标准),根据我读到的一篇论文,一种实用的方法来检测由于浮点数引起的舍入错误是计算三次。

一次使用正常舍入规则,然后始终向下舍入,最后始终向上舍入。如果最后数字相当接近,则计算可能合理。

根据 Kahan 的说法,像 "coffins" 这样的可爱想法(对于每个浮点运算产生一个范围而不是单个值给出 min/max 值)只是行不通,因为它们过于悲观而且你最终得到无限大的范围。这肯定符合我从执行此操作的 C++ boost 库中获得的经验,而且它也非常 慢。

因此,当我过去使用 ERP 软件时,我从 Kahan 的阅读中了解到我们应该使用小数来消除 0.1 + 0.2 != 0.3 之类的 "stupid" 错误,但我意识到仍然存在错误的其他来源但消除它们超出了我们在计算、存储和能力水平方面的能力。

希望对您有所帮助

PS。这是一个复杂的话题,我曾经在某个时候更改框架时出现回归错误。我深入研究,发现错误来自于旧框架中的抖动使用旧式 x86 FPU 指令,而在新的抖动中它依赖于 SSE/AVX 指令。切换到 SSE/AVX 有很多好处,但丢失的一件事是旧式 FPU 指令在内部使用 80 位浮点数,只有当浮点数离开 FPU 时,它们才会四舍五入为 64 位,而 SSE/AVX 在内部使用 64 位,这意味着框架之间的结果不同。