为什么这个 hashCode() 方法被认为很差?
Why is this hashCode() method considered poor?
这是“”的后续问题。有许多有趣的评论。有些我很了解;其他少
为什么这种 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;
}
}
}
问题是,坦率地说,当输入范围较小时,它无法正常工作。当你有字符串之类的东西时它工作正常,但不适用于小整数。
您可以考虑使用像 Murmur 这样的哈希算法。如果你可以使用像Guava这样的第三方库,这可能是
return Hashing.murmur3_32().newHasher().putInt(k1).putInt(k2).hash().asInt();
你的哈希函数可以写成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 库感兴趣,该库提供良好的通用线性探测哈希图。
我是告诉你这里很穷的人之一。我给了你原因:“250,000 个可能的 Node
值它只有 15969 个哈希码。”
如果您的 Node
项目应该或多或少均匀分布在 0 ≤ k1
< 500 和 0 ≤ k2
< 500 范围内,那么您有 250,000 个可能节点值。
一个好的散列函数应该为您提供对于这 250,000 个值尽可能唯一的散列码。也就是说,理想情况下,一个好的哈希函数应该为 k1
和 k2
.
的每个组合提供不同的值
散列函数不需要是唯一的,因为在许多情况下这是不可能的 - 如果您的对象具有数万亿和数万亿种可能的组合,当然您不能将所有这些组合映射到不同的整数。
您使用的标准散列函数适用于那种对象。如果你的对象均匀分布且可能性范围很大,那么这种哈希函数最终将使用所有可能的整数值,这是它能做的最好的事情。
但在您的特定情况下,您有 250,000 种组合,可以使用函数 500 * k1 + k2
轻松地用单个整数表示。一个完全唯一的散列函数是理想的。
并且您使用的 "standard" 哈希函数表现不佳,因为在如此小的整数范围内,它会将其中许多映射到相同的值,并且您最终只有 15,969 个唯一的哈希码。这意味着您的许多 Node
对象将映射到相同的哈希码。 (每个代码 250,000/15,969
!)。所以你会遇到很多哈希冲突。
哈希冲突越多,哈希映射的性能就越差,因为哈希映射的良好性能很大程度上依赖于同一哈希桶中尽可能少的键。而哈希桶是由哈希码决定的。
这是“
为什么这种 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;
}
}
}
问题是,坦率地说,当输入范围较小时,它无法正常工作。当你有字符串之类的东西时它工作正常,但不适用于小整数。
您可以考虑使用像 Murmur 这样的哈希算法。如果你可以使用像Guava这样的第三方库,这可能是
return Hashing.murmur3_32().newHasher().putInt(k1).putInt(k2).hash().asInt();
你的哈希函数可以写成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 库感兴趣,该库提供良好的通用线性探测哈希图。
我是告诉你这里很穷的人之一。我给了你原因:“250,000 个可能的 Node
值它只有 15969 个哈希码。”
如果您的 Node
项目应该或多或少均匀分布在 0 ≤ k1
< 500 和 0 ≤ k2
< 500 范围内,那么您有 250,000 个可能节点值。
一个好的散列函数应该为您提供对于这 250,000 个值尽可能唯一的散列码。也就是说,理想情况下,一个好的哈希函数应该为 k1
和 k2
.
散列函数不需要是唯一的,因为在许多情况下这是不可能的 - 如果您的对象具有数万亿和数万亿种可能的组合,当然您不能将所有这些组合映射到不同的整数。
您使用的标准散列函数适用于那种对象。如果你的对象均匀分布且可能性范围很大,那么这种哈希函数最终将使用所有可能的整数值,这是它能做的最好的事情。
但在您的特定情况下,您有 250,000 种组合,可以使用函数 500 * k1 + k2
轻松地用单个整数表示。一个完全唯一的散列函数是理想的。
并且您使用的 "standard" 哈希函数表现不佳,因为在如此小的整数范围内,它会将其中许多映射到相同的值,并且您最终只有 15,969 个唯一的哈希码。这意味着您的许多 Node
对象将映射到相同的哈希码。 (每个代码 250,000/15,969
!)。所以你会遇到很多哈希冲突。
哈希冲突越多,哈希映射的性能就越差,因为哈希映射的良好性能很大程度上依赖于同一哈希桶中尽可能少的键。而哈希桶是由哈希码决定的。