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 四核处理器上运行。

4

2 回答 2

13

在几乎任何有快速模式和安全模式的情况下,您都会找到某种权衡。否则一切都会以快速安全模式运行:-)。

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

我会说你的解释是最有可能的。将其置于安全模式,看看不确定性是否消失。那肯定会告诉你。

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

根据您的更新:

英特尔在此处有一份文档,其中解释了他们在安全模式下不允许做的一些事情,包括但不限于:

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

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

这在该链接文档的第 15 页上(简要地)介绍了向量化(“问题:不同的结果在同一处理器上的相同数据上重新运行相同的二进制文件”)。

我的建议是决定您是否需要原始的咕噜声或结果的总体再现性,然后根据此选择模式。

于 2009-06-09T06:27:12.953 回答
0

如果您的程序是并行化的,就像它可能在四核上运行一样,那么它很可能是不确定的。

想象一下,您有 4 个处理器将浮点值添加到同一个内存位置。那么你可能会得到

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

或者

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

或任何其他可能的顺序。

哎呀,你甚至可能得到

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

如果编译器足够好。

不幸的是,浮点加法不是可交换的或关联的。实数算术是,但浮点不是,因为舍入、上溢和下溢。

因此,并行 FP 计算通常是不确定的。“经常”,因为程序看起来像

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

将是不确定的,因为每次花费的时间可能会有很大差异——您无法预测操作的顺序。(如果线程交互,则更糟。)

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

当然,许多关心确定性的人所做的就是在整数或定点上工作以避免这个问题。我特别喜欢可以添加浮点数的超级累加器,512、1024 或 2048 位数字,而不会出现舍入错误。


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

除非...您正在使用动态语言。这执行了重新排序 FP 计算的优化,随着时间的推移而变化。

或者除非......真的很远:Itanium 有一些特性,比如 ALAT,即使是单线程编码也具有不确定性。您不太可能受到这些影响。

于 2012-05-02T02:44:58.540 回答