F# 在非确定性浮点计算方面是不是受到相同的 C# 警告?
Posted
技术标签:
【中文标题】F# 在非确定性浮点计算方面是不是受到相同的 C# 警告?【英文标题】:Does F# suffer from same C# caveats on non-deterministic floating point calculation?F# 在非确定性浮点计算方面是否受到相同的 C# 警告? 【发布时间】:2020-05-20 14:23:29 【问题描述】:C#浮点代码的结果会导致不同的结果。
这个问题不是关于为什么0.1 + 0.2 != 0.3
以及浮点机器数固有的不精确性。
这与 相同的 C# 代码,具有相同的目标体系结构(例如 x64)可能会导致 不同的结果这一事实有关,具体取决于实际情况使用的机器/处理器。
这个问题和这个有直接关系:Is floating-point math consistent in C#? Can it be?,里面讨论了C#问题。
作为参考,C# 规范中的this paragraph 明确说明了该风险:
浮点运算可以以比运算结果类型更高的精度执行。例如,某些硬件架构支持“扩展”或“长双精度”浮点类型,其范围和精度比双精度类型更大,并使用这种精度更高的类型隐式执行所有浮点运算。只有在性能成本过高的情况下,才能使此类硬件架构以较低的精度执行浮点运算,而不是要求实现同时丧失性能和精度,C# 允许将更高精度的类型用于所有浮点运算.除了提供更精确的结果之外,这很少有任何可衡量的影响
事实上,我们实际上在仅使用 double
的算法中经历了 ~1e-14
数量级的差异,我们担心这种差异会传播到使用此结果的其他迭代算法,依此类推,使我们的结果对于我们在我们的领域(医学成像研究)的不同质量/法律要求,不能始终如一地重现。
C# 和 F# 共享相同的 IL 和公共运行时,但是据我了解,它可能更多是由编译器驱动的,这对于 F# 和 C# 是不同的。
我觉得自己不够聪明,无法理解问题的根源是否是双方共同的,或者如果 F# 有希望,我们是否应该跳入 F# 来帮助我们解决这个问题。
TL;DR
C# 语言规范中明确描述了这种不一致问题。我们还没有在 F# 规范中找到等效项(但我们可能没有在正确的位置进行搜索)。
F# 在这方面是否更加一致?
即如果我们切换到 F#,是否可以保证在跨架构的浮点计算中获得更一致的结果?
【问题讨论】:
我猜这取决于所使用的 IL 语言和编译器以外的阶段。如果适用于 F# 和 C# 的规则不同,我会感到非常惊讶。 你可以看到here基本的F#原语decimal
、float32
、single
、float
和double
都映射到常规的.NET浮点类型,这意味着它们将具有与 C# 完全相同的限制和性能特征。如果您使用的是其他类型,那么我不知道。
基本的 IL 指令也是一样的,虽然 F# 编译器可能会优化和生成与 C# 略有不同的代码序列,但应该没有太大差异。许多人看到的一个常规问题是,如果可以以这样一种方式优化代码,使其可以完全在 cpu 上运行,没有内存存储/加载,那么它可以导致更好的临时精度,因为 cpu通常具有比用于存储在内存中的类型具有更高精度的寄存器。但是 F# 映射到 IL,它由与 C# 相同的引擎进行 JIT,因此应该没有太大差异。
@juharr:这个问题提到了医学成像应用。这些将涉及各种数学运算,而不是受限的运算,例如,只用钱做加法、减法和简单的乘法。无数值格式,二进制或十进制,定点或浮点,可以避免一般的舍入误差。 1/3 不能用十进制表示。成像将使用正弦、余弦和其他运算,结果无法以十进制格式表示。使用小数不适合算术。
@BentTranberg 请详细说明。这里需要修复什么?运行时架构和编译器如何避免插入非确定性?
【参考方案1】:
简而言之; C# 和 F# 共享相同的运行时,因此以相同的方式进行浮点数计算,因此当涉及到浮点数计算时,您将在 F# 中看到与在 C# 中相同的行为。
0.1 + 0.2 != 0.3
的问题涵盖了大多数语言,因为它来自 二进制 浮点数的 IEEE 标准,其中 double
就是一个示例。在二进制浮点数中,0.1、0.2 等无法精确表示。这是一些语言支持像 0x1.2p3
这样的十六进制浮点文字的原因之一,它可以精确地表示为二进制浮点数(0x1.2p3
在十进制数字系统中等于 9
btw)。
许多内部依赖 double
的软件,例如 Microsoft Excel 和 Google Sheet,采用各种作弊手段使数字看起来不错,但通常在数字上并不正确(我不是专家,我只是读过有点卡汉)。
在 .NET 和许多其他语言中,通常有一个 decimal
数据类型是十进制浮点数,确保 0.1 + 0.2 = 0.3
为真。但是,它不能保证 1/3 + 1/3 = 2/3
和 1/3
不能在十进制数字系统中精确表示。由于没有支持decimal
的硬件,它们往往速度较慢,此外.NET decimal
不符合 IEEE,这可能是也可能不是问题。
如果你有分数并且你有很多可用的时钟周期,你可以在 F# 中使用BigInteger
实现一个“大理性”。但是,分数很快就会变得非常大,并且不能代表评论中提到的 12 次根,因为根的结果通常是无理数(即不能表示为有理数)。
我想您可以象征性地保留整个计算并尝试尽可能长时间地保留精确值,然后非常仔细地计算最终数字。可能很难做到正确,而且很可能很慢。
我读过一点 Kahan(他共同设计了 8087 和 IEEE 浮点数标准),根据其中一篇论文,我读到了一种实用的方法来检测浮点数引起的舍入误差是计算三次。
一次是正常的舍入规则,然后是总是向下舍入,最后是总是向上舍入。如果最后的数字相当接近,则计算可能是正确的。
根据 Kahan 的说法,“棺材”之类的可爱想法(对于每个浮点运算产生一个范围,而不是给出最小值/最大值的单个值)只是不起作用,因为它们过于悲观,你最终得到的范围是无限大。这当然符合我在执行此操作的 C++ boost 库中的经验,而且它也非常很慢。
因此,当我过去使用 ERP 软件时,我从 Kahan 读到的内容中建议我们应该使用小数来消除像 0.1 + 0.2 != 0.3
这样的“愚蠢”错误,但我意识到还有其他错误来源,但要消除它们在计算、存储和能力水平上超出了我们的水平。
希望对你有帮助
PS。这是一个复杂的话题,我曾经在某个时候更改框架时遇到过回归错误。我深入研究了它,发现错误来自旧框架中的抖动使用旧式 x86 FPU 指令,而在新抖动中它依赖于 SSE/AVX 指令。切换到 SSE/AVX 有很多好处,但丢失的一件事是旧式 FPU 指令在内部使用 80 位浮点数,并且只有当浮点数离开 FPU 时,它们才四舍五入为 64 位,而 SSE/AVX 使用 64 位在内部,这意味着框架之间的结果不同。
【讨论】:
感谢您的回答。我对浮点数的舍入“问题”非常满意,只要它是可重现的(相同的输入,相同的输出)。问题是相同的二进制和相同的输入在我的机器和我的同事的机器上没有给出相同的输出。 (相同的操作系统,但可能不同的内部 CPU 架构)。我认为最后一段实际上是关于主题的。 您是否有导致问题的可共享代码示例 不幸的是,不是现在,它是专有代码,我需要在一些同事的帮助下进行一些实验(以查看他们机器上的差异)。我可以看出,这些计算涉及在double[255]
上进行的几十或几百次迭代的几个循环,并且它们使用 4 个基本操作以及一些 Log10 和 exp10。
我认为FPU in CPU is to blame、GPU(如果使用的话)或操作系统不太可能,但请检查。我还要检查 32 位和 64 位(我注意到)、.NET 版本、VS 版本、C# 和编译器版本、任何库版本、任何 SDK、超线程、Bios 设置、未经优化的发布、并行化是否会影响计算顺序。是复制了同一个 exe,还是只是在本地编译和运行的源代码?以上是关于F# 在非确定性浮点计算方面是不是受到相同的 C# 警告?的主要内容,如果未能解决你的问题,请参考以下文章