什么原因会导致确定性过程产生浮点错误

6

我已经阅读了这个问题,可以确定使用相同输入(在相同的硬件上,使用相同的编译器)进行浮点运算的进程应该是确定性的。我正在研究一个不满足这一条件的情况,并试图确定是什么原因导致了这种情况。

我已经编译了一个可执行文件,并向其提供完全相同的数据,在单台机器上运行(非多线程),但出现了约为3.814697265625e-06的误差。经过仔细搜索,我发现这实际上等于1/4^9 = 1/2^18 = 1/262144,接近于32位浮点数的精度级别(大约是7位数字,根据维基百科)。

我怀疑这与已应用于代码的优化有关。我正在使用英特尔C++编译器,并将浮点推测设置为快速而不是安全或严格。这会使浮点进程不确定吗?还有其他优化等可能会导致这种行为吗?

编辑:根据Pax的建议,我重新编译了代码,并将浮点推测设置为安全,现在得到了稳定的结果。这使我可以澄清这个问题-浮点推测实际上是做什么的,以及如何在应用于完全相同的输入时导致同一二进制文件(即一个编译,多次运行)生成不同的结果?

@Ben,我正在使用Intel(R) C++ 11.0.061 [IA-32]进行编译,并在Intel四核处理器上运行。


使用哪种处理器和编译器?请告知。 - Ben
如果你已经找出了是哪个标志导致了这个问题,为什么不直接查看编译器文档呢? - Tal Pressman
@Tal - 我在文档中很难找到任何信息(它只是说快速启用fps,安全/严格禁用它)。我最理解的是,fps允许操作重排序(ac + bc => c*(a+b)),但这些都是编译时优化,生成的二进制文件仍应确定性,我真的想知道为什么它不是。 - Jamie Cook
当您在单核上运行可执行文件时,不确定性是否会消失? - TonJ
2个回答

13

在几乎任何存在快速模式和安全模式的情况下,都会有某种权衡。否则,所有东西都将以快速安全模式运行 :-).

而且,如果使用相同的输入获得不同的结果,则您的流程是确定性的,无论您多么相信它(尽管有经验证据)。

我会说你的解释最有可能。将其放入安全模式并查看非确定性是否消失。这将让你确定。

至于是否存在其他优化,如果您在相同的硬件上使用相同的编译器/链接器和这些工具的相同选项进行编译,则应生成相同的代码。除了快速模式(或由于宇宙射线导致的内存中的位烂,但这非常不太可能),我看不到其他可能性。

根据您的更新:

英特尔有一份文档here,其中解释了他们在安全模式下不允许执行的一些操作,包括但不限于:

  • 重新组合:(a+b)+c -> a+(b+c)
  • 零折叠:x + 0 -> x, x * 0 -> 0
  • 倒数乘:a/b -> a*(1/b)

虽然您指出这些操作是编译时定义的,但英特尔芯片非常聪明。它们可以重新排列指令以保持多CPU设置中的流水线充满,因此,除非代码明确禁止此类行为,否则运行时的事情可能会发生变化(而不是编译时),以保持全速运行。

这在链接文档的第15页("Issue: different results re-running the same binary on the same data on the same processor")中简要介绍了矢量化问题。

我的建议是根据需要原始动力还是完全可重现的结果来选择模式。


感谢您的好解释和资源。您提供的文件链接确实说明了,这个问题(全局堆栈地址和对齐可能会因为当前运行进程之外的事件而改变)已经在11.x系列的英特尔编译器中得到了修复(我正在使用该版本)。不过,我认为您很可能已经找到了答案,即在多个CPU和许多打开的应用程序下运行时,存在某种指令重新排序的情况。再次感谢。 - Jamie Cook

0
如果您的程序是并行化的,例如在四核上运行,则可能是不确定性的。
想象一下,您有4个处理器将浮点值添加到同一内存位置。那么您可能会得到:
(((InitialValue+P1fp)+P2fp)+P3fp)+P4fp

或者

(((InitialValue+P2fp)+P3fp)+P1fp)+P4fp

或者任何其他可能的排序。

说不定你甚至会得到

 InitialValue+(P2fp+P3fp)+(P1fp+P4fp)

如果编译器足够好。

不幸的是,浮点数加法不具有可交换性或结合律。实数算术具备这些特性,但浮点数不具备,原因在于舍入、溢出和下溢。

正因如此,平行浮点运算常常是不确定的。"常常"是因为程序看起来像

  on each processor
    while( there is work to do ) {
       get work
       calculate result
       add to total 
    }

会是非确定性的,因为每个操作所需的时间可能差异很大 - 你无法预测操作的顺序。(如果线程相互交互,则更糟。)

但并非总是如此,因为有一些确定性的并行编程风格。

当然,许多关心确定性的人所做的是使用整数或定点数来避免问题。我特别喜欢超级累加器,512、1024或2048位数字可以将浮点数添加到其中,而不会出现舍入误差。


对于单线程应用程序:编译器可能会重新排列代码。不同的编译可能会得出不同的答案。但是任何特定的二进制文件都应该是确定性的。

除非...你正在使用动态语言。它执行重新排序FP计算的优化,这些优化随时间变化。

或者除非...真的很长的机会:Itanium有一些功能,比如ALAT,使得即使是单线程编码也是不确定的。你不太可能受到这些影响。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接