通常,Java会根据给定呼叫方遇到的实现数量来优化虚拟呼叫。当您查看myCode
时,可以在我results的benchmark中轻松看到这一点,这是一个返回存储的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
可以通过手动调度得到帮助,我很好奇,而且可能很多。
有几个分支完美地完成了这项工作:
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
证明确实可以。
看起来好极了,至少在本说明中
调用任何扩展Base的类的hashCode()与调用Object.hashCode()相同,如果你在Base中添加一个显式hashCode来限制潜在的调用Base的调用目标,这就是它在字节码中的编译方式。 hashCode()方法
我不确定发生了什么,但宣布Base.hashCode()
再次使hashCode
具有竞争力。
好的,提供Base#hashCode
的具体实现有帮助,但是,JIT必须知道它永远不会被调用,因为所有子类都定义了它们自己(除非另外一个子类被加载,可以导致去优化,但这对JIT来说并不是什么新鲜事。)
所以看起来错失了优化机会#1。
提供Base#hashCode
的抽象实现的工作方式相同。这是有道理的,因为它提供了确保不需要进一步查找,因为每个子类必须提供它自己的(它们不能简单地从祖父母继承)。
仍然有两个以上的实现,myCode
要快得多,编译器必须做一些不理想的事情。也许错过了优化机会#2?
答案 0 :(得分:4)
hashCode
在java.lang.Object
中定义,因此在您自己的班级中定义它并不会做太多事情。 (它仍然是一种定义的方法,但没有区别)
JIT有多种方法可以优化呼叫站点(在本例中为hashCode()
):
虚拟调用没有内联,需要通过虚拟方法表间接,并且几乎可以确保缓存未命中。缺少内联实际上需要通过堆栈传递参数的完整函数存根。总的来说,真正的性能杀手是无法内联和应用优化。
请注意:调用扩展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调用更快。
电子。