为什么哈希码比类似的方法慢?更新更新 2更新 3

2022-09-01 06:16:03

通常,Java 根据给定调用端遇到的实现数量来优化虚拟调用。这可以在我的基准测试结果中很容易看出,当你看,这是一个返回存储的微不足道的方法。有一个微不足道的myCodeint

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=trueinstanceof

到目前为止,一切都很清楚,但是,行为却大不相同。特别是,在三种情况下,它的速度要慢8-10倍。任何想法为什么?hashCode

更新

我很好奇穷人是否可以通过手动调度得到帮助,而且可以很多。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不像其他方法那样优化哈希码”仍然存在,并证明它确实可以。hashCode2

更新 2

看起来最好的是正确的,至少有这个音符

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

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

results2

更新 3

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

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

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

对于两个以上的实现,仍然快得多,以至于编译器必须做一些次优的事情。也许是错过了优化机会#2?myCode


答案 1

hashCode在 中定义,因此在你自己的类中定义它根本无济于事。(它仍然是一个定义的方法,但它没有区别)java.lang.Object

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

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

虚拟调用不是内联的,需要通过虚拟方法表进行间接寻址,并虚拟确保缓存未命中。缺少内联实际上需要完整的函数存根,参数通过堆栈传递。总体而言,真正的性能杀手是无法内联和应用优化。

请注意:调用任何扩展Base的类都与调用相同,如果您在Base中添加显式哈希代码,这将限制潜在的调用目标调用,则它是在字节码中编译的方式。hashCode()Object.hashCode()Base.hashCode()

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

额外的好处是:在加载新类时,JIT必须取消优化现有的调用站点。


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


答案 2

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


推荐