使用浮动时如何获得一致的程序行为?

时间:2012-03-08 05:26:45

标签: c++ c floating-point

我正在编写一个以离散步骤进行的模拟程序。模拟由许多节点组成,每个节点都有一个与之关联的浮点值,每个节点都会重新计算。结果可以是正数,负数或零。

在结果为零或更少的情况下发生。到目前为止,这似乎很简单 - 我可以为每个节点做这样的事情:

if (value <= 0.0f) something_happens();

然而,在我最近对程序进行的一些更改之后出现了一个问题,在该程序中我重新安排了某些计算完成的顺序。在一个完美的世界中,在重新安排之后,数值仍然会相同,但由于浮点表示的不精确,它们的出现略有不同。由于每个步骤的计算取决于前一步骤的结果,因此随着模拟的进行,结果中的这些微小变化会累积成更大的变化。

这是一个简单的示例程序,演示了我所描述的现象:

float f1 = 0.000001f, f2 = 0.000002f;
f1 += 0.000004f; // This part happens first here
f1 += (f2 * 0.000003f);
printf("%.16f\n", f1);

f1 = 0.000001f, f2 = 0.000002f;
f1 += (f2 * 0.000003f);
f1 += 0.000004f; // This time this happens second
printf("%.16f\n", f1);

该程序的输出是

0.0000050000057854
0.0000050000062402

即使加法是可交换的,所以两个结果应该是相同的。注意:我完全理解为什么会发生这种情况 - 这不是问题。问题在于,这些变化可能意味着有时一个曾经在步骤N中出现负值的值,触发something_happens(),现在可能在一两步之前或之后出现负值,这可能导致整体模拟结果非常不同,因为something_happens()有很大的影响。

我想知道的是,是否有一种很好的方法可以决定何时应该触发something_happens(),这不会受到重新排序操作导致的计算结果微小变化的影响,从而使我的程序的较新版本将与旧版本保持一致。

我到目前为止唯一能够想到的解决方案是使用像这样的值epsilon:

if (value < epsilon) something_happens();

但是因为结果中的微小变化随着时间的推移而累积,我需要使epsilon相当大(相对而言)以确保变化不会导致something_happens()在不同的步骤上被触发。还有更好的方法吗?

我已经阅读了this excellent article关于浮点比较,但我没有看到所描述的任何比较方法在这种情况下如何帮助我。

注意:不能使用整数值。


编辑提出了使用双精度而不是浮点数的可能性。这不会解决我的问题,因为变化仍然存在,它们只是一个较小的量级。

5 个答案:

答案 0 :(得分:4)

我已经使用模拟模型2年了,而epsilon方法是比较你的花车的最佳方法。

答案 1 :(得分:3)

通常,如果需要使用浮点数,则使用合适的epsilon值。以下是一些可能有用的内容:

  • 如果您的值在已知范围内,并且您不需要分区,则可以缩放问题并对整数使用精确操作。一般情况下,条件不适用。
  • 变体是使用有理数来进行精确计算。这仍然限制了可用的操作,并且通常具有严重的性能影响:您可以交换性能以确保准确性。
  • 可以更改舍入模式。这可以用于计算间隔而不是单个值(可能具有由向上舍入,向下舍入和最接近舍入的3个值)。同样,它不会对所有事情都有效,但你可能会得到一个错误估计。
  • 跟踪值和一些操作(可能的多个计数器)也可用于估计错误的当前大小。
  • 要尝试使用不同的数字表示形式(floatdouble,间隔等),您可能希望将模拟实现为为数字类型参数化的模板。
  • 在使用浮点运算时,有很多关于估计和最小化错误的书籍。这是数学数学的主题。

大多数情况下我都会对上面提到的一些方法进行简要的实验,并得出结论认为该模型不精确且不费心。此外,使用float做其他事情可能会产生更好的结果,但由于内存占用量增加和使用SIMD操作的机会较小,即使使用double也太慢了。

答案 2 :(得分:0)

如果绝对必须是浮点数,那么使用epsilon值可能有所帮助,但可能无法消除所有问题。我建议使用双打来代码中你知道肯定会有变化。

另一种方法是使用浮点数来模拟双打,有很多技术,最基本的是使用2个浮点数并做一些数学运算来将大部分数字保存在一个浮点数中,其余部分用于其他(看到一个很棒的指南,如果我找到它我会链接它。)

答案 3 :(得分:0)

我建议您单步执行 - 最好是在装配模式下 - 通过计算,同时在计算器上执行相同的算术运算。您应该能够确定哪些计算排序产生的质量低于您预期的结果以及哪些结果有效。您将从中学习,并可能在将来编写更有序的计算。

最后 - 考虑到您使用的数字示例 - 您可能需要接受这样一个事实,即您将无法进行相等比较。

对于epsilon方法,您通常需要为每个可能的指数使用一个epsilon。对于单精度浮点格式,您需要256个单精度浮点值,因为指数为8位宽。一些指数将是异常的结果,但为了简单起见,拥有256个成员向量比进行大量测试更好。

执行此操作的一种方法可以是在指数为0的情况下确定基础epsil,即要比较的值在1.0 <= x <1的范围内。 2.0。最好选择epsilon作为基数2,即可以用单精度浮点格式精确表示的值 - 这样你就可以准确地知道你正在测试什么,并且不必考虑将epsilon中的问题舍入为好。对于指数-1,您将使用基础epsilon除以2,-2除以4,依此类推。当您接近指数范围的最低和最高部分时,逐渐耗尽精度 - 逐位 - 因此您需要注意极值会导致epsilon方法失败。

答案 4 :(得分:0)

当然你应该使用双打而不是花车。这可能会显着减少翻转节点的数量。

通常,使用epsilon阈值仅在比较两个浮点数的相等性时才有用,而不是在比较它们时要查看哪个更大。所以(对于大多数模型,至少)使用epsilon根本不会获得任何东西 - 它只会改变翻转节点的集合,它不会使该集合变小。如果你的模型本身很混乱,那就太乱了。