浮点不准确性有多确定性?

时间:2008-11-30 08:11:55

标签: c# silverlight math floating-point

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

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

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

我目前在Silverlight工作,但有兴趣知道这个问题是否可以回答。

更新:初始答案表明是,但显然这并不完全清楚,如所选答案的评论中所述。看起来我将不得不做一些测试,看看会发生什么。

10 个答案:

答案 0 :(得分:20)

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

我知道的具体问题:

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

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

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

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

答案 1 :(得分:17)

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

这些问题及其他问题的长期答案可以在浮点数的最佳参考资料中找到,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相同的类别中。

最后,如果您在相同的初始输入上执行相同的序列浮点计算,那么事情应该可以完全重放。确切的顺序可能会根据您的编译器/ os /标准库而改变,因此您可能会以这种方式获得一些小错误。

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

答案 2 :(得分:8)

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

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

准确性不准确。考虑一个int与一个浮点数。两者通常占用相同的字节数(4)。然而,每个数字可以存储的最大值是完全不同的。

  • int:大约20亿
  • float:3.40282347E38(相当大)

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

答案 3 :(得分:4)

此外,虽然Goldberg是一个很好的参考,但原始文字也是错误的: IEEE754不具备可移植性。我不能强调这一点,因为基于略读文本这个陈述的频率是多少。该文档的更高版本包括a section that discusses this specifically

  

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

答案 4 :(得分:2)

很抱歉,但我不禁想到每个人都错过了这一点。

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

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

我的朋友不是模拟。如果由于四舍五入和精确度导致的微小差异而得到截然不同的结果,则可能没有任何结果具有任何有效性。仅仅因为你可以重复结果并不会使它更有效。

在任何包含测量或非整数计算的非平凡现实世界问题中,引入微小错误以测试算法的稳定性总是一个好主意。

答案 5 :(得分:2)

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位上不起作用,反之亦然,但在同一个拱门上运行时可以正常工作。

答案 6 :(得分:2)

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

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

    例如,在您的情况下,如果您记录模拟的初始状态和输入,然后安装更新CLR的服务包,则下次运行时您的模拟可能无法完全重放。

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

答案 7 :(得分:1)

HM。由于OP要求C#:

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

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

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

答案 8 :(得分:0)

这不是你问题的完整答案,但这里有一个例子,证明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. 将构建配置设置为“Release”并构建,但不要在Visual Studio中运行。

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

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

  8. 请注意,窗口显示“右绝缘接头”。
  9. 此行为已在同事的计算机上得到确认。请注意,如果满足以下任何条件,窗口将始终显示“Right Insulated Joint”:exe是从Visual Studio中运行的,exe是使用Debug配置构建的,或者在项目属性中取消选中“Prefer 32-bit”。

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

答案 9 :(得分:-4)

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

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

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

遵守FP规则1:永远不要使用if(某事==某事)比较。第二条IMO将与ascii到fp或fp与ascii(printf,scanf等)有关。与硬件相比,存在更多的准确性和错误问题。

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

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

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