Java hashCode():覆盖本机实现的速度更快?

bcf*_*bcf 7 java performance jmh

我有点惊讶的是,该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;
}
}
Run Code Online (Sandbox Code Playgroud)

或者,考虑一个完全相同的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;
}
}
Run Code Online (Sandbox Code Playgroud)

接着,下面的江铃控股基准的结果表明,对我来说,从默认的hashCode()方法的Object类几乎是50倍速度较慢比(貌似更复杂)实现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;
}
}
Run Code Online (Sandbox Code Playgroud)

事实上,总结研究结果表明,在调用hashCode()BookWithHash类是一个数量级比调用更快hashCode()Book类(参见下面的完整的江铃控股输出): 总结 JMH

我对此感到惊讶的原因是我理解默认Object.hashCode()实现(通常)是对象的初始内存地址的散列,这(至少对于内存查找)我希望在微体系结构级别非常快. 这些结果似乎向我表明,与上面给出的简单覆盖相比,内存位置散列是 中的瓶颈Object.hashCode()。我会感谢其他人对我的理解以及可能导致这种令人惊讶的行为的见解。


完整的 JMH 输出:

完整的 JMH 输出

apa*_*gin 5

你误用了 JMH,所以基准分数没有多大意义。

  1. 通常不需要在基准测试中循环运行某些东西。JMH 以一种防止 JIT 编译器过度优化被测量代码的方式运行基准循环本身。
  2. 需要通过调用Blackhole.consume或从方法返回结果来消耗被测量代码的结果和副作用。
  3. 代码的参数通常从@State变量中读取,以避免常量折叠和常量传播。

在您的情况下,BookWithHash对象是瞬态的:JIT 意识到对象不会转义,并且完全消除了分配。此外,由于某些对象字段是常量,因此 JIT 可以hashCode通过使用常量而不是读取对象字段来简化计算。

相反,默认值hashCode依赖于对象标识。这就是Book不能消除分配的原因。因此,您的基准测试实际上是将 20000 个对象(注意Double对象)的分配与局部变量和常量的一些算术运算进行比较。毫不奇怪,后者要快得多。

另外要考虑的一点是,identity 的第一次调用hashCode比后续调用慢得多,因为需要先生成 hashCode 并放入对象头中。这反过来又需要调用 VM 运行时。第二次和随后的调用hashCode只会从对象头中获取缓存的值,这确实会快得多。

这是比较 4 种情况的更正基准:

  • 获取(生成)新对象的身份哈希码;
  • 获取现有对象的身份哈希码;
  • 计算新创建对象的覆盖哈希码;
  • 计算现有对象的覆盖哈希码。
@State(Scope.Benchmark)
public class HashCode {

    int id = 123;
    String title = "Jane Eyre";
    String author = "Charlotte Bronte";
    Double price = 14.99;

    Book book = new Book(id, title, author, price);
    BookWithHash bookWithHash = new BookWithHash(id, title, author, price);

    @Benchmark
    public int book() {
        return book.hashCode();
    }

    @Benchmark
    public int bookWithHash() {
        return bookWithHash.hashCode();
    }

    @Benchmark
    public int newBook() {
        return (book = new Book(id, title, author, price)).hashCode();
    }

    @Benchmark
    public int newBookWithHash() {
        return (bookWithHash = new BookWithHash(id, title, author, price)).hashCode();
    }
}
Run Code Online (Sandbox Code Playgroud)
Benchmark                 Mode  Cnt   Score   Error  Units
HashCode.book             avgt    5   2,907 ± 0,032  ns/op
HashCode.bookWithHash     avgt    5   5,052 ± 0,119  ns/op
HashCode.newBook          avgt    5  74,280 ± 5,384  ns/op
HashCode.newBookWithHash  avgt    5  14,401 ± 0,041  ns/op
Run Code Online (Sandbox Code Playgroud)

结果表明,获取现有对象的身份 hashCode 明显比在对象字段上计算 hashCode 快(2.9 vs. 5 ns)。然而,生成一个新的身份 hashCode 是一个非常缓慢的操作,即使与对象分配相比也是如此。