【发布时间】:2019-01-06 21:59:47
【问题描述】:
为什么不首先在 equals() 的底层使用 hashCode() 来预检查相等性?
快速草稿测试:
@Fork(value = 1)
@Warmup(time = 1)
@Measurement(time = 1)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Benchmark)
public class Main {
@Param({
"o", // differ size
"oooooooooooooooooo1", // same size, differ last symbol
"oooooooooooooooooo2" // same content
})
String string1;
@Param({
"oooooooooooooooooo2"
})
String string2;
@Benchmark
public void stringEquals(Blackhole bh) {
bh.consume(string1.equals(string2));
}
@Benchmark
public void myEquals(Blackhole bh) {
bh.consume(myEquals(string1, string2));
}
boolean myEquals(String str1, String str2){
if (str1.hashCode()==str2.hashCode()) {
return str1.equals(str2);
}
return false;
}
}
结果:
Benchmark (string1) (string2) Mode Cnt Score Error Units
Main.myEquals o oooooooooooooooooo2 avgt 5 5.552 ± 0.094 ns/op
Main.myEquals oooooooooooooooooo1 oooooooooooooooooo2 avgt 5 5.626 ± 0.173 ns/op
Main.myEquals oooooooooooooooooo2 oooooooooooooooooo2 avgt 5 14.347 ± 0.234 ns/op
Main.stringEquals o oooooooooooooooooo2 avgt 5 6.441 ± 1.076 ns/op
Main.stringEquals oooooooooooooooooo1 oooooooooooooooooo2 avgt 5 13.596 ± 0.348 ns/op
Main.stringEquals oooooooooooooooooo2 oooooooooooooooooo2 avgt 5 13.663 ± 0.126 ns/op
如您所见,我们在“大小相同,最后一个符号不同”的情况下得到了很大的加速。
我认为在 String.equals() 的底层检查 hashCode() 相等性应该取代检查 length() 相等性,因为它需要相同的时间:
@Benchmark
public void emptyTest(Blackhole bh) {
bh.consume(0);
}
@Benchmark
public void stringLength(Blackhole bh) {
bh.consume(string2.length());
}
@Benchmark
public void stringHashCode(Blackhole bh) {
bh.consume(string2.hashCode());
}
Benchmark (string2) Mode Cnt Score Error Units
Main.emptyTest oooooooooooooooooo2 avgt 5 3.702 ± 0.086 ns/op
Main.stringHashCode oooooooooooooooooo2 avgt 5 4.832 ± 0.421 ns/op
Main.stringLength oooooooooooooooooo2 avgt 5 5.175 ± 0.156 ns/op
PS 我感觉我的测量方法可能是错误的,所以欢迎任何 cmets。此外,哈希值保存在 String 中,这也可能会产生一些误导性的结果...
UPD1:正如@AdamSiemion 提到的,我们需要在每次调用基准方法时重新创建字符串以避免散列码:
String str1, str2;
@Setup(value = Level.Invocation)
public void setup(){
str1 = string1;
str2 = string2;
}
@Benchmark
public void stringEquals(Blackhole bh) {
bh.consume(str1.equals(str2));
}
@Benchmark
public void myEquals(Blackhole bh) {
bh.consume(myEquals(str1, str2));
}
Benchmark (string1) (string2) Mode Cnt Score Error Units
Main.myEquals o oooooooooooooooooo2 avgt 5 29.417 ± 1.430 ns/op
Main.myEquals oooooooooooooooooo1 oooooooooooooooooo2 avgt 5 29.635 ± 2.053 ns/op
Main.myEquals oooooooooooooooooo2 oooooooooooooooooo2 avgt 5 37.628 ± 0.974 ns/op
Main.stringEquals o oooooooooooooooooo2 avgt 5 29.905 ± 2.530 ns/op
Main.stringEquals oooooooooooooooooo1 oooooooooooooooooo2 avgt 5 38.090 ± 2.933 ns/op
Main.stringEquals oooooooooooooooooo2 oooooooooooooooooo2 avgt 5 36.966 ± 1.642 ns/op
因此,对于“大小相同,最后一个符号不同”的情况,我们仍有将近 30% 的加速。
UPD2 正如@DanielPryden 提到的str1 = string1 不会创建新字符串。所以我们需要明确地这样做:
@Setup(value = Level.Invocation)
public void setup(){
str1 = new String(string1);
str2 = new String(string2);
}
Benchmark (string1) (string2) Mode Cnt Score Error Units
Main.myEquals o oooooooooooooooooo2 avgt 5 61.662 ± 3.068 ns/op
Main.myEquals oooooooooooooooooo1 oooooooooooooooooo2 avgt 5 85.761 ± 7.766 ns/op
Main.myEquals oooooooooooooooooo2 oooooooooooooooooo2 avgt 5 92.156 ± 8.851 ns/op
Main.stringEquals o oooooooooooooooooo2 avgt 5 30.789 ± 0.731 ns/op
Main.stringEquals oooooooooooooooooo1 oooooooooooooooooo2 avgt 5 38.602 ± 1.212 ns/op
Main.stringEquals oooooooooooooooooo2 oooooooooooooooooo2 avgt 5 38.921 ± 1.816 ns/op
所以,现在我们得到了预期:使用hashCode() 总是比equals() 慢。这具有整体意义(正如下面 cmets 中提到的@Carcigenicate):hashCode() 需要通过 char[] 进行完全遍历以产生哈希。我认为这可能是 hashCode() 内部的一些内在因素使其更快,但事实并非如此。
因此,如果检查预先计算的 hash 的存在并进行比较,仍然可以提高 equals() 的速度:
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length
// new code begins
&& (hash==0 || anotherString.hash==0 || hash==anotherString.hash)) {
// new code ends
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
如果字符串相等(用于检查hash 字段),我们会得到一些小的(?)减速,但如果字符串长度相同但内容不同并且已经预先计算哈希值,也会得到加速。
不幸的是,我无法测试它,因为我无法更改 String 类的源代码。
【问题讨论】:
-
我对这里使用的基准测试工具一无所知,但我认为它不应该有所作为,或者甚至可能更昂贵地预先检查哈希值。字符串散列函数 afaik 需要字符串的完整迭代,然后
equals检查可能需要 second 迭代。你会做一个完整的迭代只是为了看看你是否需要做另一个迭代。除非之前已经计算过哈希。 -
因为相同的哈希码只会确定它们可能相等。
System.out.println("FB".hashCode() == "Ea".hashCode());,然后您必须进一步测试以确定它们是否真的存在。 -
@ArtsiomChapialiou:这是不正确的。 Java 中引用类型的语义对于可变或不可变对象的行为没有不同。使用
=运算符never 的赋值操作会创建一个新对象(装箱转换除外,此处不适用)。str1 = string1表示您现在有两个 variables 都引用 same String 对象。所有属于java.lang.Object子类型的类型都是引用类型。String类型的变量不是字符串,而是对字符串的引用。 -
@ArtsiomChapialiou:我不会在这里和你争论。我建议你打开一个关于字符串复制的新问题。您有一个根本的误解:您混淆了 assignment 和 mutation。我很乐意写一个答案来解释实际发生的事情,但是这个评论空间太小了。
-
为了使情况更加复杂,jvm(s) 通常具有字符串等于代码的内在函数。这意味着 jvm 基本上在平台优化版本中“交换”,而不是您在那里看到的代码。如果在 cpu 上可用,它就开始使用 SSE 4.2 指令,例如早在 2009 年左右。见bugs.openjdk.java.net/browse/JDK-6761600
标签: java string equals hashcode