为什么hashCode比类似的方法慢?

时间:2014-06-14 18:32:27

标签: java performance hashcode

通常,Java会根据给定呼叫方遇到的实现数量来优化虚拟呼叫。当您查看myCode时,可以在我resultsbenchmark中轻松看到这一点,这是一个返回存储的int的简单方法。这是一个微不足道的

static abstract class Base {
    abstract int myCode();
}

有几个相同的实现,如

static class A extends Base {
    @Override int myCode() {
        return n;
    }
    @Override public int hashCode() {
        return n;
    }
    private final int n = nextInt();
}

随着实现次数的增加,方法调用的时序从0.4 ns增加到1.2 ns(两次实现)到11.6 ns,然后慢慢增长。当JVM看到多个实现时,即使用preload=true,时序略有不同(因为需要进行instanceof测试)。

到目前为止,一切都很清楚,但hashCode表现得相当不同。特别是,在三种情况下,它的速度要慢8-10倍。知道为什么吗?

更新

如果穷人hashCode可以通过手动调度得到帮助,我很好奇,而且可能很多。

timing

有几个分支完美地完成了这项工作:

if (o instanceof A) {
    result += ((A) o).hashCode();
} else if (o instanceof B) {
    result += ((B) o).hashCode();
} else if (o instanceof C) {
    result += ((C) o).hashCode();
} else if (o instanceof D) {
    result += ((D) o).hashCode();
} else { // Actually impossible, but let's play it safe.
    result += o.hashCode();
}

请注意,编译器避免了对两个以上实现的优化,因为大多数方法调用比简单字段加载要昂贵得多,并且与代码膨胀相比,增益会很小。

原始问题" 为什么JIT不像其他方法那样优化hashCode "遗体和hashCode2证明确实可以。

更新2

看起来好极了,至少在本说明中

  

调用任何扩展Base的类的hashCode()与调用Object.hashCode()相同,如果你在Base中添加一个显式hashCode来限制潜在的调用Base的调用目标,这就是它在字节码中的编译方式。 hashCode()方法

我不确定发生了什么,但宣布Base.hashCode()再次使hashCode具有竞争力。

results2

更新3

好的,提供Base#hashCode具体实现有帮助,但是,JIT必须知道它永远不会被调用,因为所有子类都定义了它们自己(除非另外一个子类被加载,可以导致去优化,但这对JIT来说并不是什么新鲜事。)

所以看起来错失了优化机会#1。

提供Base#hashCode抽象实现的工作方式相同。这是有道理的,因为它提供了确保不需要进一步查找,因为每个子类必须提供它自己的(它们不能简单地从祖父母继承)。

仍然有两个以上的实现,myCode要快得多,编译器必须做一些不理想的事情。也许错过了优化机会#2?

5 个答案:

答案 0 :(得分:4)

hashCodejava.lang.Object中定义,因此在您自己的班级中定义它并不会做太多事情。 (它仍然是一种定义的方法,但没有区别)

JIT有多种方法可以优化呼叫站点(在本例中为hashCode()):

  • 无覆盖 - 静态调用(完全没有虚拟) - 具有完全优化的最佳案例场景
  • 2个站点 - 例如ByteBuffer:精确类型检查然后静态分派。类型检查非常简单,但根据使用情况,可能会或可能不会由硬件预测。
  • 内联缓存 - 当在调用者体中使用了很少的不同类实例时,也可以使它们内联 - 这可能是某些方法可能被内联,有些可能通过虚拟表调用。内联预算不是很高。 这就是问题中的情况 - 一个名为hashCode()的不同方法将使用内联缓存,因为只有四个实现,而不是v-table
  • 通过该调用者主体添加更多类会在编译器放弃时产生真正的虚拟调用。

虚拟调用没有内联,需要通过虚拟方法表间接,并且几乎可以确保缓存未命中。缺少内联实际上需要通过堆栈传递参数的完整函数存根。总的来说,真正的性能杀手是无法内联和应用优化。

请注意:调用扩展Base的任何类的hashCode()与调用Object.hashCode()相同,如果添加显式hashCode,这就是它在字节码中的编译方式在Base中限制可能的调用目标,调用Base.hashCode()

太多的类(在JDK本身中)被覆盖hashCode()所以在没有内联的HashMap结构的情况下,调用是通过vtable执行的 - 即缓慢。

作为额外奖励:在加载新课程时,JIT必须取消优化现有的呼叫站点。


我可能会尝试查找一些来源,如果有人有兴趣进一步阅读

答案 1 :(得分:3)

这是一个已知的性能问题: https://bugs.openjdk.java.net/browse/JDK-8014447
它已在JDK 8中修复。

答案 2 :(得分:1)

我可以证实这些发现。请参阅这些结果(省略重新编译):

$ /extra/JDK8u5/jdk1.8.0_05/bin/java Main
overCode :    14.135000000s
hashCode :    14.097000000s

$ /extra/JDK7u21/jdk1.7.0_21/bin/java Main
overCode :    14.282000000s
hashCode :    54.210000000s

$ /extra/JDK6u23/jdk1.6.0_23/bin/java Main
overCode :    14.415000000s
hashCode :   104.746000000s

通过反复调用类SubA extends Base的方法获得结果。 方法overCode()hashCode()相同,两者都只返回一个int字段。

现在,有趣的部分:如果将以下方法添加到类Base

@Override
public int hashCode(){
    return super.hashCode();
}

hashCode的执行时间与overCode的执行时间不再相同。

Base.java:

public class Base {
private int code;
public Base( int x ){
    code = x;
}
public int overCode(){
return code;
}
}

SubA.java:

public class SubA extends Base {
private int code;
public SubA( int x ){
super( 2*x );
    code = x;
}

@Override
public int overCode(){
return code;
}

@Override
public int hashCode(){
    return super.hashCode();
}
}

答案 3 :(得分:0)

我正在看你的不变量进行测试。它已将scenario.vmSpec.options.hashCode设置为0.根据this幻灯片显示(幻灯片37),这意味着Object.hashCode将使用随机数生成器。这可能就是为什么JIT编译器对优化对hashCode的调用不太感兴趣,因为它认为它可能不得不求助于昂贵的方法调用,这会抵消避免vtable查找的任何性能提升。

这也可能是设置Base拥有自己的哈希码方法可以提高性能的原因,因为它可以防止落入Object.hashCode

http://www.slideshare.net/DmitriyDumanskiy/jvm-performance-options-how-it-works

答案 4 :(得分:-2)

hashCode()的语义比常规方法更复杂,因此调用hashCode()时JVM和JIT编译器必须比调用常规虚方法时更多工作。

一个特性对性能有负面影响:在null对象上调用hashCode()是有效的并返回零。这需要一个分支而不是常规调用,这本身就可以解释你已经建立的性能差异。

请注意,由于引入了具有此语义的Object.hashCode(target),因此它似乎仅来自Java 7。了解您测试此问题的版本以及您是否在Java6上具有相同的版本将会很有趣。

另一个特性会对性能产生积极影响:如果您不提供自己的hasCode()实现,JIT编译器将使用内联哈希码计算代码,该代码比常规编译的Object.hashCode调用更快。

电子。

相关问题