【问题标题】:How can I get consistent program behavior when using floats?使用浮点数时如何获得一致的程序行为?
【发布时间】:2012-03-25 16:17:23
【问题描述】:

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

如果结果为零或更小,就会发生某些事情。到目前为止,这似乎很简单——我可以为每个节点做这样的事情:

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 关于浮点比较的内容,但我看不出所描述的任何比较方法在这种情况下对我有什么帮助。

注意:不能选择使用整数值。


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

【问题讨论】:

  • 如果微小的变化会导致输出发生很大的变化,那不就是告诉您您的结果准确性低吗? (另外:为什么浮动不加倍?)
  • 小心:printf("%.16f\n", f1); 这是一个意想不到的副作用:它会将您的浮点数转换为双加非有效数字。一个浮点数,我认为,最大精度为 7 位。
  • 对浮点值求和的标准方法是:对它们进行排序并从最小到最大求和,这会降低精度。也使用 double 而不是 float。
  • @J.N.您可以通过将 printfs 替换为 printf("%x\n",(*(int*)&amp;f1)) 来验证结果是否不同。在第一种情况下,输出将是 36a7c5b9,而在第二种情况下,36a7c5ba - 最后一个十六进制数字不同。
  • 你绝对不能打破对先前值的依赖吗?或者类似于Kahan summation algorithm 中的方法来补偿精度损失?

标签: c++ c floating-point


【解决方案1】:

我使用仿真模型已经 2 年了,而 epsilon 方法是比较您的浮点数的最明智的方法。

【讨论】:

    【解决方案2】:

    通常,如果您需要使用浮点数,则可以使用合适的 epsilon 值。以下几点可能会有所帮助:

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

    我所知道的大多数情况下,我都对上述一些方法进行了简单的实验,并得出结论认为该模型无论如何都不精确,因此不必费心。此外,使用 float 以外的其他方法可能会产生更好的结果,但速度太慢,即使使用 double,因为内存占用增加了一倍并且使用 SIMD 操作的机会更小。

    【讨论】:

    • 在某些情况下,“规范化”(?) 会有所帮助。假设一个物体正在移动或旋转,并且它的新坐标每次都根据上次坐标重新计算。随着时间的推移,随着误差的累积,物体可能会开始失去其原始形状或大小。考虑到预期的形状和大小,可以通过适当地重新计算/调整坐标(每次或偶尔)来解决此问题。
    【解决方案3】:

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

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

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

    执行此操作的一种方法可能是在指数为 0 的情况下确定您的基本 epsilon,即要与之比较的值在 1.0

    【讨论】:

      【解决方案4】:

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

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

      【讨论】:

        【解决方案5】:

        当然,您应该使用双精度数而不是浮点数。这可能会显着减少翻转节点的数量。

        通常,使用 epsilon 阈值仅在比较两个浮点数是否相等时有用,而不是在比较它们以查看哪个更大时有用。因此(至少对于大多数模型而言)使用 epsilon 根本不会为您带来任何好处——它只会改变翻转节点的集合,不会使该集合变小。如果你的模型本身是混乱的,那么它就是混乱的。

        【讨论】:

          猜你喜欢
          • 2011-01-21
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2012-02-02
          • 2021-03-06
          • 2011-05-24
          相关资源
          最近更新 更多