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

通常,Java会根据给定呼叫端遇到的实现数量来优化虚拟呼叫。 在我的基准testing 结果中可以很容易地看到,当你看看myCode ,这是一个简单的方法返回存储的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 ,时序会略有不同(因为需要testing的instanceof )。

到目前为止,清楚的是, hashCode行为有点不同。 特别是在三种情况下,速度要慢8-10倍。 任何想法为什么?

UPDATE

我很好奇,如果这个可怜的hashCode可以通过手工调度得到帮助,而且可能会很多。

定时

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

 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 ”, hashCode2certificate了它确实可以。

更新2

看起来bestsss是正确的,至less在这个笔记

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

我不完全确定发生了什么,但声明Base.hashCode()使得hashCode再次具有竞争力。

结果2

更新3

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

所以它看起来像是一个错过的优化机会#1。

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

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

hashCode是在java.lang.Object中定义的,所以在你自己的类中定义它并不是什么大事。 (仍然是一个确定的方法,但没有区别)

JIT有几种方法来优化呼叫站点(在这种情况下是hashCode() ):

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

虚拟调用不是内联的,并且需要通过虚拟方法的间接方向,并且实际上确保caching未命中。 缺乏内联实际上需要具有通过堆栈传递的参数的全function存根。 总的来说,当真正的性能杀手是无法内联和应用优化。

请注意:任何扩展Base类的调用hashCode()与调用Object.hashCode()都是一样的,这就是它在字节码中编译的方式,如果你在Base中添加一个明确的hashCode来限制调用Base.hashCode()的潜在调用目标Base.hashCode()

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

作为额外的奖励:在加载新class级时,JIT必须对现有呼叫站点进行优化。


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

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

我可以证实这个发现。 查看这些结果(重新编译省略):

 $ /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(); } } 

我正在考虑你的testing不变式。 它有scenario.vmSpec.options.hashCode设置为0.根据这个幻灯片(幻灯片37),这意味着Object.hashCode将使用随机数字发生器。 这可能是JIT编译器对优化对hashCode调用不太感兴趣的原因,因为它认为它可能不得不求助于昂贵的方法调用,这会抵消任何性能提升,避免vtable查找。

这也可能是为什么设置Base有自己的散列码方法可以提高性能,因为它可以避免传递到Object.hashCode

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

hashCode()的语义比常规方法更复杂,所以当你调用hashCode()时,JVM和JIT编译器必须做更多的工作,而不是调用常规的虚方法。

一个特性会对性能产生负面影响:在空对象上调用hashCode()是有效的并返回零。 这需要比常规调用更多的分支,这本身可以解释您所build立的性能差异。

请注意,由于引入了带有这种语义的Object.hashCode(target),所以只能从Java 7中看到。 知道你在哪个版本testing过这个问题,以及你是否在Java6上有相同的例子会很有趣。

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

E.