java为什么哈希代码比类似的方法慢?
通常,Java会根据给定调用端遇到的实现数量来优化虚拟调用。在我的benchmark的results中可以很容易地看到这一点,当您查看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
时,计时略有不同(因为需要进行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
看来bestsss是对的,至少在这张纸条上是这样
calling hashCode() of any class extending Base is the same as calling Object.hashCode() and this is how it compiles in the bytecode, if you add an explicit hashCode in Base that would limit the potential call targets invoking Base.hashCode().
我不能完全确定到底发生了什么,但是宣布Base.hashCode()
会使hashCode
再次具有竞争力
更新3
好的,提供Base#hashCode
的具体实现会有所帮助,但是,JIT必须知道它永远不会被调用,因为所有子类都定义了自己的子类(除非加载另一个子类,这可能导致去优化,但这对JIT来说并不是什么新鲜事)
因此,这看起来像是错过了一次优化机会
提供Base#hashCode
的抽象实现也是如此。这是有意义的,因为它确保了不需要进一步的查找,因为每个子类都必须提供自己的(它们不能简单地从祖父母那里继承)
但是对于两个以上的实现,myCode
的速度要快得多,以至于编译器必须做一些subobtimal的事情。也许错过了一次优化机会#2
# 1 楼答案
这是一个已知的性能问题: https://bugs.openjdk.java.net/browse/JDK-8014447
它已在JDK 8中修复
# 2 楼答案
我可以证实调查结果。见这些结果(省略重新编译):
结果是通过反复调用类
SubA extends Base
的方法获得的。 方法overCode()
与hashCode()
相同,两者都只返回一个int字段现在,有趣的部分是:如果将以下方法添加到类库中
hashCode
的执行时间与overCode
的执行时间不再不同基地。爪哇:
苏巴。爪哇:
# 3 楼答案
hashCode
是在java.lang.Object
中定义的,因此在您自己的类中定义它根本没有什么作用。(这仍然是一个已定义的方法,但没有区别)JIT有几种优化呼叫站点的方法(在本例中
hashCode()
):虚拟调用不是内联的,需要通过虚拟方法表进行间接寻址,并且实际上确保了缓存未命中。缺少内联实际上需要通过堆栈传递参数的完整函数存根。总的来说,真正的性能杀手是无法内联和应用优化
请注意:调用任何扩展基类的
hashCode()
与调用Object.hashCode()
是一样的,如果在基类中添加显式hashCode,将限制调用Base.hashCode()
的潜在调用目标,那么它在字节码中的编译方式就是这样的太多的类(在JDK本身中)被
hashCode()
重写,因此在非内联HashMap类似结构的情况下,调用是通过vtable执行的,即慢额外的好处是:在加载新类时,JIT必须对现有呼叫站点进行去优化
如果有人有兴趣进一步阅读,我可以尝试查找一些资料来源
# 4 楼答案
hashCode()的语义比常规方法更复杂,因此调用hashCode()时JVM和JIT编译器必须比调用常规虚拟方法时做更多的工作
一种特殊性对性能有负面影响:对null对象调用hashCode()是有效的,并且返回零。这需要比常规调用多进行一次分支,而常规调用本身就可以解释性能差异
请注意,由于引入了对象,这似乎只适用于Java 7。具有此语义的哈希代码(目标)。了解您在哪个版本上测试这个问题,以及您是否会在Java6上测试这个问题,这将是一件有趣的事情
另一个特性对性能有积极的影响:如果不提供自己的hasCode()实现,JIT编译器将使用比常规编译对象更快的内联hashcode计算代码。hashCode调用
E
# 5 楼答案
我在看你的不变量做测试。它已将
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