为什么Java HashMap复制构造函数会影响浮点精度?

时间:2012-03-08 21:00:20

标签: java floating-point copy-constructor

我有一些代码在浮点数地图上计算线性组合,并遇到了使用复制构造函数的有趣副作用。

如果我计算两个地图中的线性值组合并将其与使用这些地图的两个副本中的值计算的线性组合进行比较,则计算实际上略有不同(在附近10 ^ -7)结果由于看起来是浮点精度。

为什么会这样?

以下是一些示例代码:

import java.util.*;

public class WTF {
    public static void main(String[] args) {
        Random rand = new Random();

        for (int c = 0; c < 1000; c++) {
            Map<String, Float> weights = new HashMap<String, Float>();
            Map<String, Float> values = new HashMap<String, Float>();

            for (int j = 0; j < 10; j++) {
                weights.put("sig" + j, Float.valueOf(rand.nextFloat()));
                values.put("sig" + j, Float.valueOf(rand.nextFloat()));
            }

            Map<String, Float> weightsCopy = new HashMap<String, Float>(weights);
            Map<String, Float> valuesCopy = new HashMap<String, Float>(values);

            float score1 = getScore(weights, values);
            float score2 = getScore(weightsCopy, valuesCopy);

            if (score1 != score2) {
                System.out.println(score1-score2);
            }
        }
    }

    public static float getScore(Map<String, Float> weights, Map<String, Float> values) {
        float score = 0.0f;
        for (String name : weights.keySet()) {
            Float weight = weights.get(name);
            Float value = values.get(name);
            score += weight.floatValue() * value.floatValue();
        }
        return score;
    }
}

更新

同样的问题也适用于putAll操作。使用它来“复制”HashMap会导致相同的浮点精度问题。

4 个答案:

答案 0 :(得分:5)

迭代顺序正在从原始地图变为副本,因为它正在重建哈希表(可能具有不同的大小)。

舍入的不同之处在于浮点数上的*+不是相当可交换/关联,并且您将获得不同的舍入误差关于您是a * (b * c)还是(a * c) * b还是(a * b) * c。由于条目和键的顺序在原件和副本之间发生变化,因此结果会出现微小的舍入差异。

如果您使用LinkedHashMap代替HashMap来确保保留的迭代顺序,则每次都应获得完全相同的结果。 (我已经在我的机器上证实了这一点。)

答案 1 :(得分:5)

地图中的顺序正在发生变化,导致操作以不同的顺序运行。简单计算的输出变化示例(注意翻转的d和e):

class WTF {
    public static void main(String[] args) {
        final float a = 0.42890447f * 0.37233013f;
        final float b = 0.2648958f * 0.05867535f;
        final float c = 0.8928169f * 0.7546882f;
        final float d = 0.0039135218f * 0.59395087f;
        final float e = 0.9114683f * 0.33522367f;

        System.out.println(a + b + c + d + e);
        System.out.println(a + b + c + e + d);
    }
}

答案 2 :(得分:0)

如果你查看浮点数,你会看到一个字节指数和一个mantisse位(左边8位)被交换,所以一位错误。 (2,384186e-07 34800000

            float ds = score1-score2;
            int bits = Float.floatToIntBits(ds);
            System.out.printf("%e %x%n", score1-score2, bits);

答案 3 :(得分:0)

添加浮点数的顺序会影响结果。由于HashMap不保证任何顺序,因此复制HashMap会导致不同的顺序,这意味着值的总和将会不同。

public static void main(String... args) throws IOException {
    List<Float> floats = new ArrayList<>();
    Random rand = new Random();
    float sum0 = 0;
    for (int i = 0; i < 1000; i++) {
        float f = rand.nextFloat() - rand.nextFloat();
        floats.add(f);
        sum0 += f;
    }
    floats.add(-sum0);

    SortedSet<Float> sums = new TreeSet<>();
    for (int i = 0; i < 200000; i++) {
        Collections.shuffle(floats, rand);
        float sum = 0;
        for (Float f : floats)
            sum += f;
        if (sums.add(sum))
            System.out.println(sum);
    }
    System.out.println("Unique sums count " + sums.size()
            + " from " + sums.first() + " to " + sums.last());
}

打印

1.8239021E-5
2.0623207E-5
-2.1278858E-5
1.847744E-5
2.18153E-5
  ....
-2.4557114E-5
-3.415346E-5
1.9788742E-5
-2.270937E-5
Unique sums count 795 from -3.4868717E-5 to 3.1232834E-5