通常情况下,Java会根据给定调用方上遇到的实现数量来优化虚拟调用。这可以在我基准测试的结果中轻松地看出,当您查看返回存储的int
的微不足道的方法myCode
时。有一个微不足道的
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纳秒逐渐增长到11.6纳秒,然后缓慢增长。当JVM看到多个实现时,即使用preload=true
,时间略有不同(因为需要进行instanceof
测试)。
到目前为止都很清楚,但是hashCode
的行为则相当不同。特别是,在三种情况下,它的速度要慢8-10倍。有任何想法吗?
更新
我很好奇是否可以通过手动分派来帮助可怜的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?”仍然存在,而hashCode2证明了它确实可以。更新2:最好sss是正确的,至少有这个笔记。调用扩展Base的任何类的hashCode()与调用Object.hashCode()相同,这是如何在字节码中编译的,如果在Base中添加显式hashCode,那将限制潜在的调用目标调用Base.hashCode()。我不完全确定发生了什么,但声明Base.hashCode()会使hashCode有竞争力。结果2:更新3:提供Base#hashCode的具体实现有所帮助,但JIT必须知道它永远不会被调用,因为所有子类都定义了自己的实现(除非加载另一个子类,这可能会导致取消优化,但对于JIT来说这并不新鲜)。因此,它看起来像是错过了优化机会1。提供Base#hashCode的抽象实现也可以工作。这是有道理的,因为它确保不需要进一步查找,因为每个子类都必须提供自己的实现(它们不能简单地从其祖父继承)。对于超过两个实现,myCode要快得多,编译器必须做一些次优的事情。也许是错过了优化机会2?
A.myCode()
和B.myCode()
之间切换。当表真正发挥作用时,就会出现10倍的减速。 - maaartinushashCode
方法受JIT的特殊处理。您可以尝试使用-XX:DisableIntrinsic=_hashCode
,或查看http://hg.openjdk.java.net/jdk8/jdk8/hotspot/file/a57a165b8296/src/share/vm/opto/library_call.cpp#l3924(和#l3977,#l4000,#l4103 ...搜索“hashCode”)。我无法指出* THE原因*,但猜测它隐藏在那里.... - Marco13