为什么这个双算术在一台机器上给出两个不同的答案?

时间:2018-02-14 10:29:33

标签: c# floating-point

在我工作的某些软件的深处,有一行代码......

double DataNoise = StatsStuff.MeanofSquares() - average * average;

示例数字:

StatsStuff.MeanofSquares() = 1.9739125181231402E-13  
average = -4.3328988592605794E-07
DataNoise = 9.6511265664977283E-15 //(State1)  
DataNoise = 9.6511265664977204E-15 //(State2)

如果我重复从GUI重新启动分析,这个计算的结果迟早会发生变化,有时是在第一次重新运行分析时,但在转换到不同的答案之前通常会给出一些一致的结果(开关变化很大的次数)。一旦软件切换到返回第二个值,它就永远不会恢复为返回第一个值。

我正在使用C#和Visual Studio,在带有i5 4570的Windows 7计算机上进行测试,如果有帮助的话。

我在Debug和Release版本中都看到了这个问题。

每次启动分析时,所有分析对象都在分析方法中重新创建,因此不应该有任何持久性。

我记录了计算中的值并且它们没有改变;我还使用BitConverter.GetBytes()检查数字是否相同。

我已经看过下面的问题以及许多其他类似的在线文章,但它们都与两台不同机器之间的差异有关。
Why does this floating point calculation give different results...

how-deterministic-is-floating-point-inaccuracy中的答案似乎表明我应该能够从单个机器和指令集中预期确定性行为,但我不会。

任何帮助解释为什么会发生这种情况和/或如何确保一致的结果将非常感激。

调试中的一些额外字节值:
输入:
平均值:48,51,51,18,221,19,157,190
MeanOfSquares:205,250,200,243,196,199,75,61

输出:
DataNoise(状态1):192,220,244,228,126,187,5,61
DataNoise(状态2):187,220,244,228,126,187,5,61

1 个答案:

答案 0 :(得分:1)

根据C# specification,关于double的操作,“使用至少双倍范围和精度进行操作......”

这意味着可以使用扩展精度计算StatsStuff.MeanofSquares(),并且可以在StatsStuff.MeanofSquares() - average * average中直接使用此扩展精度结果。如果StatsStuff.MeanofSquares()的计算产生的结果在扩展精度上略有不同,则当数字仅打印17位时,差异可能不明显。

一个重要的线索是,显示的两个结果恰好是通过使用单独的double乘法和第一个结果的加法显示的输入值计算得到的结果和融合double乘法和第二个结果的加法或long double算术。这表明正在使用不同的指令来评估这两个结果。具体做法是:

  • m为最接近1.9739125181231402E-13的double值。
  • a成为最接近-4.3328988592605794E-07的double值。
  • 您显示的第一个结果,9.6511265664977283E-15,等于a*a中计算double的结果,从m中的double减去产品,并转换结果为17位小数。
  • 您显示的第二个结果,9.6511265664977204E-15,等于用精确数学计算m-a*a的结果,然后四舍五入到double(与C的fma(a, -a, m)一样)并转换结果到17位小数。它也等于a*a中计算long double的结果,从m中减去产品,将结果四舍五入为double,并将其转换为17位小数。

执行这些不同操作的唯一方法是使用不同的指令。因此,这表明double DataNoise = StatsStuff.MeanofSquares()在不同时间被编译为不同的指令。一种可能性是该语句在源代码中出现多次。另一个是编译器内联包含它的函数,因此它在不同的上下文中被不同地编译。

由于该问题未提供可重现的示例或有关语句double DataNoise = StatsStuff.MeanofSquares() - average * average的上下文的任何信息,因此无法做出明确的答案。

虽然这些数字匹配单独对比融合和双对长双模式,这强烈建议不同的指令用于不同的结果,但仍有可能使用一个指令序列来计算{{ 1}}但是表达式的输入值会有所不同,可能会计算出StatsStuff.MeanofSquares() - average * average的额外精度,这在您打印的有限数字中是不可见的。 如果您的软件是多线程的,它可能会将问题划分为子问题并与多个线程并行执行。当这些线程返回结果时,它可以组合结果以产生StatsStuff.MeanofSquares()的最终结果。由于线程可以在不同的运行中以不同的顺序完成,因此结果可以以不同的顺序组合。这意味着在操作中使用了不同的数据,因此结果可能不同。 (例如,在两位十进制中,加上2​​1 + 4.9 + 90得到26 + 90(21 + 4.9正好是25.9,所以四舍五入到两位数产生26)然后是120,但是加上21 + 90 + 4.9得到110( 111轮到110)然后110轮(110 + 4.9轮到110轮)。

另一种可能性是软件有一个错误导致它使用未初始化的数据,而这些数据会影响结果。

如果问题是表达式以不同方式进行评估,则可能的解决方法可能是将中间结果分配给临时变量:

StatsStuff.MeanofSquares()

我猜想这样的分配会导致将每个表达式舍入到double t0 = average*average; double t1 = StatsStuff.MeanofSquares(); double Mean = t1 - t0; 。我没有在C#规范中看到这个明确的声明,但它是C中的规则,C#编译器也可以这样做。如果是这样,这可能会大大降低观察到不同最终结果的频率,但可能无法完全消除它们。

(由于C#不能满足您的目的,您应该让Microsoft知道,您应该寻求其他语言和其他编译器。)