【问题标题】:Why is this hashCode() method considered poor?为什么这个 hashCode() 方法被认为很差?
【发布时间】:2015-03-02 00:37:26
【问题描述】:

这是“Using Java 7 HashMap in Java 8”的后续问题。有许多有趣的 cmets。有些我很理解;其他人少。

为什么这个hashCode() 方法被认为很差?

乍一看,我认为这是合理的。也许 17 可以增加到 31。否则,它似乎遵循来自Arrays.hashCode(Object[]) 的普遍接受的公式。一种猜测:它适用于项目数量相对较少(小于 10.000)的一般情况,但对于非常大的集合(1.000.000 或更多)表现不佳。

这是原始代码:(全部包含在内以提供一些上下文。)

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 performance hashmap hashcode


    【解决方案1】:

    我是告诉你那里很穷的人之一。我给了你原因:“250,000 个可能的 Node 值它只有 15969 个哈希码。”

    如果您的 Node 项目应该或多或少均匀分布在 0 ≤ k1 k2

    一个好的散列函数应该为这 250,000 个值提供尽可能唯一的散列码。也就是说,理想情况下,一个好的哈希函数应该为k1k2 的每个组合提供不同的值。

    哈希函数不需要是唯一的,因为在许多情况下这是不可能的 - 如果您的对象具有数万亿个可能的组合,当然您不能将所有这些组合映射到不同的整数。

    您使用的标准哈希函数适用于那种对象。如果您的对象分布均匀且可能性范围很大,那么这种散列函数最终会使用所有可能的整数值,这是它所能做的最好的事情。

    但在您的特定情况下,您有 250,000 种组合,可以使用函数 500 * k1 + k2 轻松表示为单个整数。一个完全唯一的哈希函数是理想的。

    而您使用的“标准”哈希函数表现不佳,因为在如此小的整数范围内,它将其中许多映射到相同的值,最终您只有 15,969 个唯一哈希码。这意味着您的许多 Node 对象将映射到相同的哈希码。 (250,000/15,969 每个代码!)。所以你会有很多哈希冲突。

    哈希冲突越多,哈希映射的性能就越差,因为哈希映射的大部分良好性能依赖于相同哈希桶中尽可能少的键。而哈希桶是由哈希码决定的。

    【讨论】:

    • 非常感谢您的回答。这正是我从 cmets 那里寻求的关于另一个问题的更深层次的解释。干杯。
    • @kevinarpe 他的解释写得很好。 :) 也许你应该选择它作为答案。
    【解决方案2】:

    你的哈希函数可以写成 31 * 17 * 31 + 31 * k1 + k2。

    您可以看到将 31 加到 k2 和 -1 加到 k1 将产生相同的哈希值。

    那么大约 1 到 500 范围内的每一对数字将有大约一打 (500 / 31) 其他具有相同哈希的对。

    在您的示例代码中完美执行的哈希函数将是 500 * k1 + k2。 (快速测试显示大约 3 倍的性能提升。)

    正如 Louis Wasserman 指出的那样,使用一位研究得很好的将军 来自库的哈希函数可能是一个安全的选择。

    至于为什么标准数组散列函数在这种情况下表现不佳(顺便说一句,IntelliJ默认生成相同的函数。)

    在这里不要求进行完整的分析,但显然散列变量的数量越大(假设它们在某种意义上是独立的)并且每个可能值的集合越大,函数的性能就越好。在您的情况下,性能很差,因为只有 2 个变量,而且它们的范围都很小。

    似乎在 Java 8 中 HashMap 实现变得更加复杂,大概是为了在某些场景中获得更好的渐近性能而对其进行了优化。这种小的附加复杂性以及性能不佳的哈希函数会导致性能下降。

    在这种情况下,linear probing hash map 可能是更适合您的算法。作为一个更简单的结构和更少的缓存未命中,它应该在读取繁重的工作负载时提供更好的性能。我对提供良好的通用线性探测哈希映射的 Java 库很感兴趣。

    【讨论】:

      【解决方案3】:

      坦率地说,问题在于当输入的范围很小时它不能很好地工作。当你有像字符串这样的东西时它工作得很好,但不适用于小整数。

      您可以考虑使用像 Murmur 这样的哈希算法。如果你可以使用像 Guava 这样的第三方库,这可能是

      return Hashing.murmur3_32().newHasher().putInt(k1).putInt(k2).hash().asInt();
      

      【讨论】:

      • 这是否意味着Objects.hash也不够用?它的算法非常相似,但并不完全相同。
      • @VGR,至少在我的环境中它似乎是完全相同的算法。
      • 将 Guava 的 Hasher 用于 2 个整数是多余的。因为它会产生垃圾。如果您想普及 Google 的库,您最好建议 @AutoValue 完成此任务
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2019-11-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-09-28
      • 1970-01-01
      相关资源
      最近更新 更多