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

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

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

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

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

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

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

我所知道的具体陷阱:

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

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

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

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

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

这些问题及其答案的长期答案可以在浮点数的最佳参考资料中找到,David Goldberg的“每个计算机科学家应该知道的关于浮点算术的内容” 。 有关详细信息,请跳至IEEE标准部分。

简要回答一下你的要点:

  • 计算和CPU状态之间的时间与此无关。

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

  • 语言,平台和操作系统也会影响事物。 为了更好地描述这一点,请参阅Jason Watkins的回答。 如果您正在使用Java,请查看Kahan 对Java浮点不足的咆哮 。

  • 太阳耀斑可能很重要,希望不经常发生。 我不会太担心,因为如果它们确实重要,那么其他一切也都搞砸了。 我会把它放在与担心EMP相同的类别中。

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

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

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

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

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

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

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

此外,虽然Goldberg是一个很好的参考,但原始文本也是错误的: IEEE754不具备可移植性 。 我不能强调这一点,因为基于略读文本这个陈述的频率是多少。 该文档的更高版本包括一个专门讨论此内容的部分 :

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

对不起,但我不禁想到每个人都错过了这一点。

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

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

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

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

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

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

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

计算仍然是确定性的,因为运行相同的编译二进制文件每次都会给你相同的结果,但是当你稍微调整一下源,优化标记或用不同的编译器编译时,所有的赌注都是关闭的可以发生。

实际上,我并没有那么糟糕,我可以在32位Linux位上重复使用不同版本的GCC进行简单的浮动指向数学,但是当我切换到64位Linux时,结果不再相同。 在32位上创建的演示录制在64位上不起作用,反之亦然,但在同一个拱上运行时可以正常工作。

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

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

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

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

请参阅Shawn Hargreaves的博客文章是浮点数学确定性吗? 有关.NET的进一步讨论。

HM。 由于OP要求C#:

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

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

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

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

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

     using System; using System.Windows; namespace WpfApplication1 { ///  /// Interaction logic for MainWindow.xaml ///  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. 请注意,窗口显示“右绝缘接头”。

这种行为在同事的机器上得到了证实。 请注意,如果满足以下任何条件,窗口将始终显示“Right Insulated Joint”:exe是从Visual Studio中运行的,exe是使用Debug配置构建的,或者在项目属性中取消选中“Prefer 32-bit”。

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

很少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数学)。 现在,现代硬件(和软件)根据定义充满了错误,所以只是尝试减少错误,而不是运行软件。