什么可能导致浮点数突然被1位关闭而没有算术变化

时间:2014-07-30 21:27:25

标签: java floating-point floating-point-precision

在进行稍微大的重构更改没有修改任何类型的算术时,我设法以某种方式更改了我的程序(基于代理的模拟系统)的输出。输出中的各种数字现在都是微不足道的数量。检查表明,这些数字的最低有效位为1位。

例如,24.198110084326416将变为24.19811008432642。每个数字的浮点表示为:

24.198110084326416 = 0 10000000011 1000001100101011011101010111101011010011000010010100
24.19811008432642  = 0 10000000011 1000001100101011011101010111101011010011000010010101

我们注意到最低有效位是不同的。

我的问题是,当我没有修改任何类型的算术时,我是如何引入这种变化的?这种变化涉及通过删除继承来简化对象(它的超类因使用不适用于此类的方法而膨胀)。

我注意到输出(在模拟的每个刻度处显示某些变量的值)有时会关闭,然后对于另一个滴答,数字是预期的,只有在下一个滴答时再次关闭(例如,在一个代理上,它的值在刻度57-83上显示出这个问题,但是对于刻度84和85是预期的,只有在刻度86时再次关闭。

我知道我们不应该直接比较浮点数。当仅将输出文件与预期输出进行比较的集成测试失败时,会注意到这些错误。我可以(也许应该)修复测试来解析文件并将解析后的双打与一些epsilon进行比较,但我仍然很好奇为什么可能引入这个问题。

编辑:

引入问题的最小变化差异:

diff --git a/src/main/java/modelClasses/GridSquare.java b/src/main/java/modelClasses/GridSquare.java
index 4c10760..80276bd 100644
--- a/src/main/java/modelClasses/GridSquare.java
+++ b/src/main/java/modelClasses/GridSquare.java
@@ -63,7 +63,7 @@ public class GridSquare extends VariableLevel
    public void addHousehold(Household hh)
    {
        assert household == null;
-       subAgents.add(hh);
+       neighborhood.getHouseholdList().add(hh);
        household = hh;
    }

@@ -73,7 +73,7 @@ public class GridSquare extends VariableLevel
    public void removeHousehold()
    {
        assert household != null;
-       subAgents.remove(household);
+       neighborhood.getHouseholdList().remove(household);
        household = null;
    }

diff --git a/src/main/java/modelClasses/Neighborhood.java b/src/main/java/modelClasses/Neighborhood.java
index 834a321..8470035 100644
--- a/src/main/java/modelClasses/Neighborhood.java
+++ b/src/main/java/modelClasses/Neighborhood.java
@@ -166,9 +166,14 @@ public class Neighborhood extends VariableLevel
    World world;

    /**
+    * List of all grid squares within the neighborhood.
+    */
+   ArrayList<VariableLevel> gridSquareList = new ArrayList<>();
+
+   /**
     * A list of empty grid squares within the neighborhood
     */
-   ArrayList<GridSquare> emptyGridSquareList;
+   ArrayList<GridSquare> emptyGridSquareList = new ArrayList<>();

    /**
     * The neighborhood's grid square bounds
@@ -836,7 +841,7 @@ public class Neighborhood extends VariableLevel
     */
    public GridSquare getGridSquare(int i)
    {
-       return (GridSquare) (subAgents.get(i));
+       return (GridSquare) gridSquareList.get(i);
    }

    /**
@@ -865,7 +870,7 @@ public class Neighborhood extends VariableLevel
    @Override
    public ArrayList<VariableLevel> getGridSquareList()
    {
-       return subAgents;
+       return gridSquareList;
    }

    /**
@@ -874,12 +879,7 @@ public class Neighborhood extends VariableLevel
    @Override
    public ArrayList<VariableLevel> getHouseholdList()
    {
-       ArrayList<VariableLevel> list = new ArrayList<VariableLevel>();
-       for (int i = 0; i < subAgents.size(); i++)
-       {
-           list.addAll(subAgents.get(i).getHouseholdList());
-       }
-       return list;
+       return subAgents;
    }

不幸的是,我无法创建一个小的,可编译的示例,因为我无法在程序之外复制此行为,也没有将这个非常庞大且纠缠不清的程序缩小到适当大小。

至于正在进行什么样的浮点运算,没有什么特别令人兴奋的。大量的加法,乘法,自然对数和幂(几乎总是以e为基数)。后两者是用标准库完成的。整个程序使用随机数,并使用正在使用的框架(Repast)中包含Random class生成。

大多数数字在1e-3到1e5的范围内。几乎没有非常大或非常小的数字。 Infinity和NaN在许多地方使用。

作为基于代理的仿真系统,许多公式被重复应用于模拟出现。评估的顺序非常重要(因为许多变量取决于首先评估的其他变量 - 例如,为了计算BMI,我们需要首先计算饮食和心脏状态)。以前的变量值在许多计算中也非常重要(所以这个问题可以在程序的早期某个地方引入,并在其余部分中进行)。

3 个答案:

答案 0 :(得分:1)

以下是浮点表达式的求值可能有所不同的几种方法:

(1)浮点处理器具有“当前舍入模式”,这可能导致最低有效位的结果不同。您可以拨打电话获取或设置当前值:向零,向-∞或向+∞舍入。

(2)听起来strictfp与C中的FLT_EVAL_METHOD有关,它指定了在中间计算中使用的精度。有时,新版本的编译器将使用与旧版本不同的方法(我被那个人咬了)。 {0,1,2}分别对应{single,double,extended}精度,除非被更高精度的操作数覆盖。

(3)与其他编译器可以使用不同的默认浮点计算方法的方式相同,不同的计算机可以使用不同的浮点计算方法。

(4)单精度IEEE浮点运算具有良好的定义性,可重复性和与机器无关。双精度也是如此。我编写了(非常谨慎)跨平台浮点测试,它使用SHA-1哈希来检查位精度的计算!但是,使用FLT_EVAL_METHOD = 2时,扩展精度用于中间计算,使用64位,80位或128位浮点运算进行各种实现,因此很难获得跨平台和交叉编译器的可重复性如果在中间计算中使用扩展精度。

(5)浮点运算不是关联的,即

(A + B) + C ≠ A + (B + C)

由于这个原因,不允许编译器重新排序浮点数的计算。

(6)操作顺序很重要。以尽可能高的精度计算大量数字之和的算法是以递增的数量级对它们求和。另一方面,如果两个数字的大小差异足够

B < (A * epsilon)

然后将它们相加是一个无操作:

A + B = A

答案 1 :(得分:0)

由于已经取消了strictfp,我会提出一个想法。

某些版本的Repast存在错误,某些随机数生成不正确*。

即使随机种子设置为相同的值,因为您的ArrayList是在代码中的不同位置创建和使用的,您可能会以不同的顺序对其中的代理进行操作。如果您有任何具有随机优先级的计划方法,则尤其如此。如果使用getAgentList()或类似命令填充subAgents列表,也会出现这种情况。实际上,您可以生成一个在您设置种子的RNG之外的随机数(/ order)。

如果执行顺序略有不同,这可以解释一步的对应关系只是为了看到其他步骤的这个小差异。

我遇到过这种情况,并且在调试时遇到了类似的问题。如果你能提供更多细节,很高兴。


*了解您正在使用哪个版本会有很多帮助(我知道我不应该在答案中要求澄清,但是没有得到代表的评论)。从你链接的API,我认为你使用旧的Repast 3 - 我使用Simphony,但答案可能仍然适用。

答案 2 :(得分:-2)

如果没有确切的源代码来重现问题,显然无法确定问题。但你的差异显示,你改变了列表处理的方式。你还提到很多简单的数学运算,比如在你的应用程序中添加。因此我的猜测是,通过更改列表,您可以更改处理内容的顺序,这可能足以改变舍入错误。

是的,没有什么可以依赖浮点变量的最低有效位,所以测试应该需要epsilons。