我有点惊讶的是 hashCode()
方法的速度似乎比下面的基准测试方法的简单重写慢50倍。
考虑一个基本的 Book
不重写的类 hashCode()
:
public class Book {
private int id;
private String title;
private String author;
private Double price;
public Book(int id, String title, String author, Double price) {
this.id = id;
this.title = title;
this.author = author;
this.price = price;
}
}
或者,考虑另一个相同的问题 Book
班级, BookWithHash
,将覆盖 hashCode()
方法使用intellij的默认实现:
public class BookWithHash {
private int id;
private String title;
private String author;
private Double price;
public BookWithHash(int id, String title, String author, Double price) {
this.id = id;
this.title = title;
this.author = author;
this.price = price;
}
@Override
public boolean equals(final Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final BookWithHash that = (BookWithHash) o;
if (id != that.id) return false;
if (title != null ? !title.equals(that.title) : that.title != null) return false;
if (author != null ? !author.equals(that.author) : that.author != null) return false;
return price != null ? price.equals(that.price) : that.price == null;
}
@Override
public int hashCode() {
int result = id;
result = 31 * result + (title != null ? title.hashCode() : 0);
result = 31 * result + (author != null ? author.hashCode() : 0);
result = 31 * result + (price != null ? price.hashCode() : 0);
return result;
}
}
然后,以下jmh基准测试的结果向我建议 Object
类的实现比 hashCode()
在 BookWithHash
班级:
public class Main {
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder().include(Main.class.getSimpleName()).forks(1).build();
new Runner(opt).run();
}
@Benchmark
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public long bookWithHashKey() {
long sum = 0L;
for (int i = 0; i < 10_000; i++) {
sum += (new BookWithHash(i, "Jane Eyre", "Charlotte Bronte", 14.99)).hashCode();
}
return sum;
}
@Benchmark
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public long bookKey() {
long sum = 0L;
for (int i = 0; i < 10_000; i++) {
sum += (new Book(i, "Jane Eyre", "Charlotte Bronte", 14.99)).hashCode();
}
return sum;
}
}
事实上,总结的结果表明 hashCode()
上 BookWithHash
类比调用快一个数量级 hashCode()
上 Book
类(请参见下面的完整jmh输出):
我对此感到惊讶的原因是我理解默认值 Object.hashCode()
实现(通常)是对象的初始内存地址的散列,我希望它(至少对于内存查找)在微体系结构级别非常快。这些结果似乎向我暗示,内存位置的散列是内存中的瓶颈 Object.hashCode()
,与上面给出的简单覆盖相比。我会很感激其他人对我的理解以及是什么导致了这种令人惊讶的行为。
完整jmh输出:
2条答案
按热度按时间lnxxn5zx1#
性能上的差异是由于您正在为每个对象创建一个新对象
hashCode()
基准中的调用,以及默认hashCode()
实现将其值缓存在对象头中,而自定义的则不会。写入对象头需要很多时间,因为它涉及本机调用。重复调用默认值
hashCode()
实现的性能比定制的要好一点。如果你设置
-XX:-UseBiasedLocking
,您将看到性能差异减小。由于有偏差的锁定信息也存储在对象头中,禁用它会影响对象布局,这是一个额外的证明。vnzz0bqm2#
你误用了jmh,所以基准分数没有多大意义。
通常不需要在基准内的循环中运行某些东西。jmh以一种防止jit编译器过度优化被度量代码的方式运行基准循环。
需要通过调用
Blackhole.consume
或者从方法返回结果。代码的参数通常从
@State
变量以避免常数折叠和常数传播。就你而言,
BookWithHash
对象是暂时的:jit实现了对象不转义,并且完全消除了分配。此外,由于一些对象字段是常量,jit可以简化hashCode
通过使用常量而不是读取对象字段进行计算。相反,违约
hashCode
依赖于对象标识。这就是为什么Book
无法消除。因此,您的基准实际上是比较20000个对象的分配(请注意Double
对象)对局部变量和常量进行一些算术运算。毫不奇怪,后者要快得多。另一个需要考虑的是,第一个身份的召唤
hashCode
要比后续调用慢得多,因为需要首先生成hashcode并将其放入对象头中。这又需要调用vm运行时。第二次和随后的呼叫hashCode
将只从对象头中获取缓存的值,这确实要快得多。下面是一个更正的基准,比较了4种情况:
获取(生成)新对象的标识码;
获取现有对象的标识码;
计算新创建的对象的重写哈希代码;
正在计算现有对象的重写哈希代码。
结果表明,获得现有对象的身份哈希码明显快于在对象字段上计算哈希码(2.9 ns vs.5 ns)。然而,生成一个新的身份哈希码是一个非常慢的操作,甚至与对象分配相比也是如此。