使用浮点运算时,如何获得一致的程序行为

How can I get consistent program behavior when using floats?

本文关键字:程序 何获得 浮点运算      更新时间:2023-10-16

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

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

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("%.16fn", f1);
f1 = 0.000001f, f2 = 0.000002f;
f1 += (f2 * 0.000003f);
f1 += 0.000004f; // This time this happens second
printf("%.16fn", f1);

该程序的输出为

0.0000050000057854
0.0000050000062402

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

我想知道的是,是否有一种好的方法来决定什么时候应该触发一些事情(),而这些事情不会受到重新排序操作导致的计算结果的微小变化的影响,这样我的程序的新版本的行为就会与旧版本一致。

到目前为止,我能想到的唯一解决方案是使用一些值epsilon,如下所示:

if (value < epsilon) something_happens();

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

我读过这篇关于浮点比较的优秀文章,但在这种情况下,我看不出所描述的任何比较方法对我有什么帮助。

注意:不能使用整数值。


编辑已经提出了使用doubles而不是float的可能性。这并不能解决我的问题,因为变化仍然存在,只是幅度较小。

我已经使用模拟模型两年了,epsilon方法是比较浮动的最明智的方法。

如果需要使用浮点数,通常使用合适的ε值。以下是一些可能有所帮助的东西:

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

据我所知,在大多数情况下,我都会用上面提到的一些方法进行简单的实验,并得出结论,无论如何,该模型都是不精确的,不需要费力。此外,除了使用float之外,做一些其他事情可能会产生更好的结果,但速度太慢,即使使用double也是如此,因为使用SIMD操作的内存占用空间增加了一倍,而且机会更小。

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

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

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

一种方法可以是在指数为0的情况下确定基ε,即要与之比较的值在1.0<=x<2.0。优选地,ε应选择为基数2,即一个可以以单精度浮点格式精确表示的值,这样你就可以准确地知道你要测试的是什么,也不必考虑ε中的舍入问题。对于指数-1,你会使用基数ε除以2,对于-2除以4,依此类推。当你接近指数范围的最低和最高部分时,你会逐渐失去精度-一点一点-所以你需要意识到极值可能会导致ε方法失败。

如果它绝对必须是浮动的,那么使用ε值可能会有所帮助,但可能不会消除所有问题。我建议对代码中的点使用double,因为你知道代码中肯定会有变化。

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

当然应该使用doubles而不是float。这可能会显著减少翻转节点的数量。

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