在Java 8中使用Java 7的HashMap

17

我已将一个Java应用程序升级到Java 8。该应用程序严重依赖于HashMaps。当我运行基准测试时,看到的结果是不可预测的。对于某些输入,应用程序比以前运行得更快,但对于较大的输入,则始终较慢。

我已经检查了分析器,发现最耗时的操作是HashMap.get。我怀疑变化是由于Java 8中的HashMap修改造成的,但这可能并不是真的,因为我还改变了其他部分。

是否有一种简单的方法,可以将原始的Java 7 HashMap钩入我的Java 8应用程序中,以便我只更改hashmap实现,看看是否仍然观察到性能方面的变化。

以下是尝试模拟我的应用程序正在执行的内容的最小程序。基本思想是需要在应用程序中共享节点。在某个运行时点上,应根据一些整数属性检索或创建节点(如果节点不存在)。以下只使用两个整数,但在实际应用程序中,我具有一个、两个和三个整数键。

import java.util.HashMap;
import java.util.Map;
import java.util.Random;

public class Test1 {

static int max_k1 = 500;
static int max_k2 = 500;

static Map<Node, Node> map;
static Random random = new Random();

public static void main(String[] args) {
    for (int i = 0; i < 15; i++) {
        long start = System.nanoTime();
        run();
        long end = System.nanoTime();
        System.out.println((end - start) / 1000_000);
    }
}

private static void run() {
    map = new HashMap<>();
    for (int i = 0; i < 10_000_000; i++) {
        Node key = new Node(random.nextInt(max_k1), random.nextInt(max_k2));
        Node val = getOrElseUpdate(key);
    }
}

private static Node getOrElseUpdate(Node key) {
    Node val;
    if ((val = map.get(key)) == null) {
        val = key;
        map.put(key, val);
    }
    return val;
}

private static class Node {

    private int k1;
    private int k2;

    public Node(int k1, int k2) {
        this.k1 = k1;
        this.k2 = k2;
    }

    @Override
    public int hashCode() {
        int result = 17;
        result = 31 * result + k1;
        result = 31 * result + k2;
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;

        if (!(obj instanceof Node))
            return false;

        Node other = (Node) obj;

        return k1 == other.k1 && k2 == other.k2;
    }
  }
}

基准测试较为简单,但这是在Java 8上运行15次的结果:

8143
7919
7984
7973
7948
7984
7931
7992
8038
7975
7924
7995
6903
7758
7627

这段内容是针对Java 7的:

7247
6955
6510
6514
6577
6489
6510
6570
6497
6482
6540
6462
6514
4603
6270

基准测试比较原始,所以我很感激如果有熟悉JMH或其他基准测试工具的人来运行它,但是从我观察的结果来看,Java 7的表现更好。有什么想法吗?


6
我认为这些变化是由于HashMap的修改导致的。不要假设。 - NimChimpsky
2
@NimChimpsky: 肯定存在一种实现差异,这会导致性能的变化。链接 - Makoto
3
Makoto提供的链接展示了Java 8的性能提升。而你所说的是:“对于某些输入,应用程序运行速度比以前更快,但对于更大的输入,它会持续变慢。”那么你可以看一下自己的hashCode()方法 - 如何生成哈希码。虽然我不太确定这一点。 - Razib
3
只有在单个哈希桶中的条目数大于8个时才会使用树——而不是整个映射表——这是一个很强的指标说明你有一个糟糕的哈希函数。 - Louis Wasserman
3
在特定范围内(两个介于0和499之间的整数),该哈希函数可能不是理想的。对于可能的250,000个“Node”值,它只有15969个哈希码。如果这是您对象的实际范围,也许您应该将哈希函数更改为“500*k1+k2”,以便在此范围内获得完全独特的哈希码。 - RealSkeptic
显示剩余25条评论
1个回答

23

您的hashCode()方法非常差。在您发布的示例中,有250000个唯一值,但只有15969个唯一的哈希码。由于存在大量冲突,Java 8会将列表与树交换。在您的情况下,这只会增加开销,因为许多元素不仅在哈希表中具有相同的位置,而且哈希码也相同。最终,该树无论如何都会变成一个链接列表。

有几种方法可以解决这个问题:

  1. 改善hashCode方法。使用 return k1 * 500 + k2; 可以解决问题。

  2. 使用THashMap。在存在冲突的情况下,使用开放寻址法可能更好。

  3. 使Node实现Comparable。这将被HashMap用于在冲突的情况下构建平衡树。


1
Java 7 表现更好的原因可能归因于其 hash() 函数提供了更好的洗牌以防止这种情况发生。 - Wickoo
1
@Ali,我认为原因是Java 7在冲突时不使用相对昂贵的树结构,而是使用线性列表(对于更多的冲突来说不太好,但在你的情况下似乎成本更低)。 - eckes
我同意 @eckes 的观点。当哈希码相同时,洗牌并不重要。树的速度较慢,因为对于相同的哈希码和非可比较元素,它会作为一个带有不必要开销的链表来操作。 - Piotr Praszmo
我认为使用identityHashCode作为决定因素时,如果没有可比较性,实际上会得到一棵平衡树。但我不确定它的表现如何,特别是当您使用新键而不是重用实例时。 - eckes
2
Java 8 看起来比较慢,因为它尝试通过改进哈希冲突的性能来提高速度的技术,在哈希函数像你这样糟糕的情况下会受到更严重的影响。 - Louis Wasserman

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接