【问题标题】:How deterministic is floating point inaccuracy?浮点不准确性的确定性如何?
【发布时间】:2010-09-24 14:59:40
【问题描述】:

我了解浮点计算存在准确性问题,并且有很多问题可以解释原因。我的问题是,如果我两次运行相同的计算,我是否可以始终依靠它来产生相同的结果?哪些因素可能会影响这一点?

  • 计算之间的时间?
  • CPU 的当前状态?
  • 不同的硬件?
  • 语言/平台/操作系统?
  • 太阳耀斑?

我有一个简单的物理模拟,想记录会话以便回放。如果可以依赖计算,那么我只需要记录初始状态以及任何用户输入,并且我应该始终能够准确地再现最终状态。如果计算不准确,开始时的错误可能会在模拟结束时产生巨大影响。

我目前在 Silverlight 工作,但我很想知道这个问题是否可以得到一般性的回答。

更新:最初的答案是肯定的,但显然这并不完全明确,正如所选答案的 cmets 中所讨论的那样。看来我必须做一些测试,看看会发生什么。

【问题讨论】:

  • 在 Silverlight 中,您正在处理 JIT 编译器 - 这意味着数学运算可能会自动利用 SSE、MMX 和其他特殊指令,并且这些或其他更改可能会修改执行指令的确切顺序:A+使用浮点值时,B+C 可能不会给出与 C+B+A 相同的结果。因此,在同一台机器上运行时,您会得到确定性的结果,但在另一台处理器上可能会得到不同的结果,甚至是稍微不同的系统配置。
  • 按精度排序的浮点数:十进制、双精度、浮点数。
  • 这取决于月相。

标签: c# silverlight math floating-point


【解决方案1】:

据我了解,只有在处理相同的指令集和编译器,并且运行的任何处理器都严格遵守相关标准(即 IEEE754)时,才能保证得到相同的结果。也就是说,除非您正在处理一个特别混乱的系统,否则运行之间的任何计算偏差都不太可能导致错误行为。

我知道的具体问题:

  1. 某些操作系统允许您以破坏兼容性的方式设置浮点处理器的模式。

  2. 浮点中间结果通常在寄存器中使用 80 位精度,但在内存中仅使用 64 位。如果以更改函数内的寄存器溢出的方式重新编译程序,则与其他版本相比,它可能返回不同的结果。大多数平台都会为您提供一种方法来强制将所有结果截断到内存中的精度。

  3. 标准库函数可能会因版本而异。我收集到在 gcc 3 vs 4 中有一些不常见的例子。

  4. IEEE 本身允许一些二进制表示不同...特别是 NaN 值,但我不记得细节了。

【讨论】:

  • @Jason Watkins:NaN 只有两种逻辑表示,安静和信号,但是 NaN 有许多二进制表示。否则 +1 好东西。
  • #1 在 Windows 上尤为重要。有一些 DirectX 版本会将 CPU 置于较低精度模式,从而导致意外结果。
  • 重新 CPU 实现,这取决于您使用的语言。 C 在您将获得的 FP 方面相当不具体。 C# 和 Java 指定了 IEEE754 语义,然后实现的工作就是隐藏处理器的实际能力。如果我在旧的 VAX 或损坏的 Pentium 上运行 Java,那么我希望看到 IEEE754 行为,尽管这不是处理器实现的,因为语言定义要求它。如果我不这样做,那么 JVM 就会按照定义被破坏。
  • @ijw:实际上,JVM 被故意破坏为双精度/浮点数。您需要 strictfp 关键字使其严格符合 IEEE754,但这可能会使程序变慢。
  • 虽然很多人认为 80 位类型是 x87 的怪癖,但实际上它被设计为比 64 位 double 在没有浮点单元的机器上使用起来更快。 “经典”Macintosh 从未使用 8x87,但它执行浮点计算的方式与 8x87 大致相同。
【解决方案2】:

简短的回答是,FP 计算完全是确定性的,根据 IEEE Floating Point Standard,但这并不意味着它们在机器、编译器、操作系统等之间完全可重现。

这些问题和更多问题的详细答案可以在可能是浮点方面最好的参考资料中找到,David Goldberg 的What Every Computer Scientist Should Know About Floating Point Arithmetic。跳到 IEEE 标准部分了解关键细节。

简要回答您的要点:

  • 计算和状态之间的时间 CPU的关系不大 这。

  • 硬件会影响事物(例如,某些 GPU 不 符合 IEEE 浮点)。

  • 语言、平台和操作系统也可以 影响事物。有关此问题的更好描述,请参阅 Jason Watkins 的回答。如果您使用 Java,请查看 Kahan 的 rant on Java's floating point inadequacies

  • 太阳耀斑可能很重要,希望如此 不经常。我不会太担心,因为如果 他们确实很重要,然后其他一切也都搞砸了。我会将这与担心EMP 归为一类。

最后,如果您在相同的初始输入上执行相同的序列浮点计算,那么事情应该完全可以重播。确切的顺序可能会根据您的编译器/操作系统/标准库而有所不同,因此您可能会遇到一些小错误。

如果您有一个数值不稳定的方法,并且您从 大约 相同但不完全相同的 FP 输入开始,您通常会遇到浮点问题。如果您的方法稳定,您应该能够在一定的公差范围内保证重现性。如果您想了解更多详细信息,请查看上面链接的 Goldberg 的 FP 文章或获取有关数值分析的介绍性文本。

【讨论】:

  • 查看我对@JaredPar 的回复,有很多事情会导致两个符合 IEEE 标准的实现的计​​算之间存在差异。说计算是确定性的并不是特别有用,因为确定性并不一定意味着可重现。
【解决方案3】:

我认为您的困惑在于浮点类型的不准确性。大多数语言都实现了IEEE floating point standard 这个标准列出了如何使用浮点/双精度中的各个位来生成数字。通常一个浮点数由一个四个字节和一个双八字节组成。

两个浮点数之间的数学运算每次都将具有相同的值(如标准中指定的那样)。

不准确之处在于精确度。考虑一个 int 与一个 float。两者通常占用相同数量的字节 (4)。然而,每个数字可以存储的最大值却大相径庭。

  • int:大约 20 亿
  • 浮点数:3.40282347E38(有点大)

区别在中间。 int,可以表示 0 到大约 20 亿之间的每个数字。浮动但是不能。它可以表示 0 到 3.40282347E38 之间的 20 亿个值。但这留下了一系列无法​​表示的值。如果数学方程达到这些值之一,则必须将其四舍五入为可表示的值,因此被认为是“不准确的”。您对不准确的定义可能会有所不同:)。

【讨论】:

  • 这掩盖了不那么明确的可复制性方面。 IEEE 做出了一定的保证,但这些保证是基于严格的假设,并没有扩展到所有操作或库函数。 @jason-watkins 很好地解释了他回答中的主要问题。
  • 底线是,如果您在同一实现(计算机/编译器/运行时)上使用有限的操作,您可能能够准确地重现结果,但结果很可能会略有不同不同的实现,甚至那些支持 IEEE-754 的实现。
  • 我认为,受 Jason 的 cmets 启发的 Robert 应该附加到这个答案中。
  • 阅读段落。此处链接的 wiki 文章中的“再现性”。摘要:IEEE 754-1985 确实保证实现之间的可重复性。 754-2008 鼓励它,但仍然没有强制它。如果您的语言使用 754,几乎可以肯定它是 2008 年之前的版本。
  • 这是对浮点精度的一个很好的解释,但它根本没有解决所提出的问题,Jason Watkin 的回答确实并且应该是我认为可以接受的答案。
【解决方案4】:

另外,虽然Goldberg 是一个很好的参考,但原文也是错误的:IEEE754 不适合移植。鉴于此陈述基于略读文本的频率,我无法强调这一点。该文档的更高版本包括a section that discusses this specifically

许多程序员可能没有意识到,即使是仅使用 IEEE 标准规定的数字格式和运算的程序也可以在不同的系统上计算出不同的结果。事实上,该标准的作者打算让不同的实现获得不同的结果。

【讨论】:

    【解决方案5】:

    对不起,但我不禁认为每个人都没有抓住重点。

    如果不准确对您正在做的事情很重要,那么您应该寻找不同的算法。

    您说如果计算不准确,则开始时的错误可能会在模拟结束时产生巨大影响。

    我的朋友不是模拟的。如果由于舍入和精度导致的微小差异,您得到的结果大不相同,那么很可能没有任何结果具有任何有效性。仅仅因为你可以重复结果并不能使它更有效。

    对于任何包含测量或非整数计算的重要现实世界问题,引入小错误以测试您的算法的稳定性始终是一个好主意。

    【讨论】:

    • 不,我认为您错过了重点。问题实际上是关于可重复性而不是准确性。
    • 在这种特殊情况下,Anthony 是对的,我正在寻找可重复性而不是准确性,因为我正在尝试创建一些有趣的东西而不是真正的“模拟”。也许在那儿可以使用更好的词...游乐场?
    【解决方案6】:

    C++ FAQ 中的这个答案可能描述得最好:

    http://www.parashift.com/c++-faq-lite/newbie.html#faq-29.18

    不仅不同的架构或编译器会给你带来麻烦,浮点数在同一个程序中的行为也已经很奇怪了。正如常见问题解答指出的那样,如果y == x 为真,那仍然可能意味着cos(y) == cos(x) 将为假。这是因为 x86 CPU 使用 80 位计算值,而该值以 64 位存储在内存中,因此您最终将截断的 64 位值与完整的 80 位值进行比较。

    计算仍然是确定性的,因为每次运行相同的编译二进制文件都会给您相同的结果,但是当您稍微调整源代码时,优化标志或使用不同的编译器编译所有赌注都是关闭,任何事情都可能发生。

    实际上,我并没有那么糟糕,我可以在 32 位 Linux 上使用不同版本的 GCC 逐位重现简单的浮点指向数学,但是当我切换到 64 位 Linux 时,结果不再相同。在 32 位上创建的演示录音无法在 64 位上运行,反之亦然,但在同一架构上运行时可以正常运行。

    【讨论】:

      【解决方案7】:

      由于您的问题被标记为 C#,因此值得强调 .NET 面临的问题:

      1. 浮点数学不具有关联性 - 也就是说,(a + b) + c 不能保证等于 a + (b + c)
      2. 不同的编译器会以不同的方式优化您的代码,这可能涉及重新排序算术运算。
      3. 在 .NET 中,CLR 的 JIT 编译器会即时编译您的代码,因此编译取决于运行时机器上的 .NET 版本。

      这意味着,您不应依赖 .NET 应用程序在不同版本的 .NET CLR 上运行时产生相同的浮点计算结果。

      例如,在您的情况下,如果您记录初始状态和模拟输入,然后安装更新 CLR 的服务包,则您的模拟在下次运行时可能不会以相同的方式重播。

      有关 .NET 的进一步讨论,请参阅 Shawn Hargreaves 的博文 Is floating point math deterministic?

      【讨论】:

        【解决方案8】:

        嗯。由于 OP 要求使用 C#:

        C# 字节码 JIT 是确定性的,还是在不同的运行之间生成不同的代码?我不知道,但我不会相信 Jit。

        我可以想到 JIT 具有一些服务质量特性并决定在优化上花费更少时间的场景,因为 CPU 正在其他地方进行大量的数字运算(想想背景 DVD 编码)?这可能会导致细微的差异,之后可能会导致巨大的差异。

        此外,如果 JIT 本身得到改进(可能作为服务包的一部分),生成的代码肯定会发生变化。 80位内部精度问题已经提到了。

        【讨论】:

          【解决方案9】:

          这不是您问题的完整答案,但这里有一个示例证明 C# 中的双重计算是不确定的。我不知道为什么,但看似无关的代码显然会影响下游双重计算的结果。

          1. 在 Visual Studio 版本 12.0.40629.00 Update 5 中创建一个新的 WPF 应用程序,并接受所有默认选项。
          2. 将 MainWindow.xaml.cs 的内容替换为:

            using System;
            using System.Windows;
            
            namespace WpfApplication1
            {
                /// <summary>
                /// Interaction logic for MainWindow.xaml
                /// </summary>
                public partial class MainWindow : Window
                {
                    public MainWindow()
                    {
                        InitializeComponent();
                        Content = FooConverter.Convert(new Point(950, 500), new Point(850, 500));
                    }
                }
            
                public static class FooConverter
                {
                    public static string Convert(Point curIPJos, Point oppIJPos)
                    {
                        var ij = " Insulated Joint";
                        var deltaX = oppIJPos.X - curIPJos.X;
                        var deltaY = oppIJPos.Y - curIPJos.Y;
                        var teta = Math.Atan2(deltaY, deltaX);
                        string result;
                        if (-Math.PI / 4 <= teta && teta <= Math.PI / 4)
                            result = "Left" + ij;
                        else if (Math.PI / 4 < teta && teta <= Math.PI * 3 / 4)
                            result = "Top" + ij;
                        else if (Math.PI * 3 / 4 < teta && teta <= Math.PI || -Math.PI <= teta && teta <= -Math.PI * 3 / 4)
                            result = "Right" + ij;
                        else
                            result = "Bottom" + ij;
                        return result;
                    }
                }
            }
            
          3. 将构建配置设置为“发布”并构建,但不要在 Visual Studio 中运行。

          4. 双击生成的exe运行它。
          5. 请注意,窗口显示“底部绝缘接头”。
          6. 现在在“字符串结果”之前添加这一行:

            string debug = teta.ToString();
            
          7. 重复步骤 3 和 4。

          8. 请注意,窗口显示“Right Insulated Joint”。

          此行为在同事的机器上得到确认。请注意,如果以下任何一项为真,则窗口始终显示“Right Insulated Joint”:exe 从 Visual Studio 中运行,exe 是使用 Debug 配置构建的,或者在项目属性中未选中“Prefer 32-bit”。

          很难弄清楚发生了什么,因为任何观察过程的尝试似乎都会改变结果。

          【讨论】:

            【解决方案10】:

            很少有 FPU 符合 IEEE 标准(尽管他们声称)。所以在不同的硬件上运行相同的程序确实会给你不同的结果。结果很可能出现在您在软件中使用 FPU 时应该避免的极端情况。

            IEEE 错误通常在软件中修补,您确定您今天运行的操作系统包含制造商提供的适当陷阱和补丁吗?操作系统更新之前或之后呢?是否删除了所有错误并添加了错误修复? C 编译器是否与所有这些同步,C 编译器是否生成正确的代码?

            对此进行测试可能是徒劳的。在您交付产品之前,您不会发现问题。

            遵守 FP 规则 1:永远不要使用 if(something==something) 比较。 IMO 的第二条规则与 ascii 到 fp 或 fp 到 ascii(printf、scanf 等)有关。那里的准确性和错误问题比硬件中的要多。

            随着每一代新硬件(密度)的出现,来自太阳的影响更加明显。我们已经在行星表面上遇到了 SEU 的问题,因此独立于浮点计算,您会遇到问题(很少有供应商会费心去关心,所以预计新硬件会更频繁地崩溃)。

            通过消耗大量逻辑,fpu 可能会非常快(单个时钟周期)。不比整数 alu 慢。不要将此与现代 fpus 与 alus 一样简单混淆,fpus 很昂贵。 (alus 同样消耗更多的乘法和除法逻辑以将其降低到一个时钟周期,但它几乎没有 fpu 大)。

            遵守上面的简单规则,多研究浮点数,了解随之而来的缺陷和陷阱。您可能需要定期检查无穷大或 nans。您的问题更有可能出现在编译器和操作系统中而不是硬件中(通常不仅仅是 fp 数学)。如今,现代硬件(和软件)根据定义充满了错误,因此请尽量减少错误,而不是运行您的软件。

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2011-01-23
              • 2018-06-02
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多