关于c#:浮点不准确性有多确定?

How deterministic is floating point inaccuracy?

我知道浮点计算存在准确性问题,有很多问题可以解释为什么。我的问题是,如果我运行相同的计算两次,我能始终依赖它来产生相同的结果吗?哪些因素可能会影响这一点?

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

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

我目前在Silverlight工作,不过我有兴趣知道这个问题是否可以得到一般性的回答。

更新:最初的答案是肯定的,但显然这并不像所选答案的评论中所讨论的那样完全清楚。看起来我得做些测试看看会发生什么。


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

我知道的具体问题:

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

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

3.)标准库功能可能在不同版本之间发生变化。我认为在GCC 3和GCC 4中有一些不常见的例子。

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


简而言之,根据IEEE浮点标准,FP计算是完全确定性的,但这并不意味着它们可以在机器、编译器、操作系统等之间完全重现。

这些问题的答案很长,而且更多的可以在关于浮点运算的最好参考文献中找到,大卫戈德伯格的《关于浮点运算的每个计算机科学家都应该知道的》。有关关键细节,请跳到IEEE标准的部分。

简单回答要点:

  • 计算和状态之间的时间CPU的性能与这个。

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

  • 语言、平台和操作系统也可以影响事物。关于这个比我能提供的更好的描述,请看杰森·沃特金斯的回答。如果您正在使用Java,请查看卡亨关于Java的浮点不足之处的咆哮。

  • 希望太阳耀斑可能很重要很少。我不会担心太多,因为如果他们真的很重要,其他的事情也都搞砸了。我会把这和担心电磁脉冲放在同一个类别。

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

通常在浮点中会遇到问题的地方是,如果您有一个数值不稳定的方法,并且从大致相同但不完全相同的fp输入开始。如果你的方法是稳定的,你应该能够保证在一定的公差范围内的再现性。如果你想要更多的细节,那就看看上面链接的goldberg的fp文章,或者在数值分析上找到一篇介绍性的文章。


我认为你的困惑在于浮点数的不准确。大多数语言实现了IEEEfloatingpoint标准。该标准规定了如何使用float/double中的单个位来生成数字。通常一个浮点由一个四字节和一个双八字节组成。

两个浮点数之间的数学运算每次都有相同的值(如标准中所规定的)。

这种不准确是精确的。考虑int与float。两者通常占用相同的字节数(4)。然而,每个数字所能存储的最大值却大不相同。

  • 利息:大约20亿
  • 浮点数:3.40282347E38(稍大一点)

差别在中间。int,可以表示0到20亿之间的每一个数字。但是,不能浮动。它可以代表20亿个介于0和3.40282347e38之间的值。但这会留下一系列无法表示的值。如果一个数学方程达到其中一个值,它必须四舍五入为一个可表示的值,因此被认为是"不准确的"。您对不准确的定义可能会有所不同:)。


另外,虽然goldberg是一个很好的参考,但原文也是错误的:ieee754不是gaurenteed,不便于携带。鉴于这句话是以略读课文为基础的,我强调得还不够。文档的后续版本包括一个专门讨论此问题的部分:

Many programmers may not realize that even a program that uses only the numeric formats and operations prescribed by the IEEE standard can compute different results on different systems. In fact, the authors of the standard intended to allow different implementations to obtain different results.


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

  • 浮点数学不是关联的——也就是说,(a + b) + c不保证等于a + (b + c)
  • 不同的编译器将以不同的方式优化代码,这可能涉及重新排序算术操作。
  • 在.NET中,clr的jit编译器将动态编译代码,因此编译取决于运行时计算机上.NET的版本。
  • 这意味着,当在不同版本的.NET CLR上运行时,不应依赖于.NET应用程序生成相同的浮点计算结果。

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

    看到肖恩·哈格里夫斯的博客帖子是浮点数学确定性吗?有关.NET的进一步讨论。


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

    http://www. PARASIFIF.COM/C++-FAQ-Lite /NexBee。

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

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

    实际上,我并没有那么糟糕,我可以在32位Linux位上用不同版本的gcc在32位Linux位上复制简单的浮点指向数学,但是当我切换到64位Linux时,结果就不再一样了。在32位上创建的演示记录在64位上不起作用,反之亦然,但在同一拱门上运行时效果良好。


    对不起,但我忍不住认为每个人都错过了要点。

    如果不精确性对你正在做的事情很重要,那么你应该寻找一种不同的算法。

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

    我的朋友不是模拟的。如果由于舍入和精度的微小差异而得到的结果有很大不同,那么很可能所有结果都没有任何有效性。仅仅因为您可以重复这个结果就不会使它变得更有效。

    对于任何包含度量值或非整数计算的非平凡的现实世界问题,最好引入小错误来测试算法的稳定性。


    嗯,既然手术室要求C:

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

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

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


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

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    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;
            }
        }
    }
  • 将生成配置设置为"release"和build,但不要在Visual Studio中运行。

  • 双击生成的exe以运行它。
  • 注意,窗口显示"底部绝缘接头"。
  • 现在在"字符串结果"之前添加此行:

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

  • 请注意,窗口显示"右绝缘接头"。
  • 这一行为在同事的机器上得到了证实。请注意,如果以下任何一项为真,则窗口始终显示"右绝缘连接":exe在Visual Studio中运行,exe是使用调试配置生成的,或者在项目属性中未选中"首选32位"。

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


    很少有FPU符合IEEE标准(尽管他们声称)。因此,在不同的硬件上运行相同的程序确实会给您带来不同的结果。结果可能出现在角落的情况下,作为在软件中使用FPU的一部分,您应该避免这种情况。

    IEEEBug通常在软件中被修补,您确定您今天运行的操作系统包括来自制造商的正确陷阱和修补程序吗?操作系统更新之前或之后怎么办?是否删除了所有错误并添加了错误修复?C编译器是否与所有这些同步?C编译器是否生成了正确的代码?

    测试这可能证明是徒劳的。在交付产品之前,您不会看到问题。

    遵守FP规则1:不要使用if(something==something)比较。第二条规则IMO必须处理ASCII到FP或FP到ASCII(printf、scanf等)。这里有比硬件更多的准确性和错误问题。

    随着每一代新硬件(密度)的出现,来自太阳的影响更加明显。我们在行星表面上已经有了SEU的问题,所以不依赖于浮点计算,您将有问题(很少有供应商愿意关心,所以预计新硬件的崩溃频率更高)。

    通过消耗大量的逻辑,FPU可能是一个非常快的(单时钟周期)。不比整数算术逻辑单元慢。不要把这和现代的FPU混淆,因为它和ALUS一样简单,FPU是昂贵的。(ALUS同样消耗更多的逻辑来进行乘法和除法运算,以将其降到一个时钟周期,但它的规模并没有FPU那么大)。

    遵守上面的简单规则,多学一点浮点数,了解伴随它的疣和陷阱。您可能需要定期检查无穷大或纳米。与硬件相比,您的问题更可能出现在编译器和操作系统中(通常不只是FP数学)。现在,根据定义,现代硬件(和软件)充满了bug,所以尽量减少bug,而不是软件运行时的bug。