【问题标题】:Why is hashCode slower than a similar method?为什么 hashCode 比类似方法慢?
【发布时间】:2014-06-14 18:32:27
【问题描述】:

通常,Java 会根据给定调用端遇到的实现数量来优化虚拟调用。这可以很容易地在我的benchmarkresults 中看到,当您查看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 是对的,至少有这个注释

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

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

更新 3

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

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

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

对于两个以上的实现,myCode 的速度要快得多,以至于编译器必须做一些不太理想的事情。也许错过了优化机会 #2?

【问题讨论】:

  • 我会强调 Base 的多种实现以及此类的扩展;它被埋没在问题中(但根本不在标题中)并且感觉大部分都迷失了。
  • 您使用的是哪个版本的卡尺?我想自己测试一下。
  • @SotiriosDelimanolis 我正在使用来自git 的那个,但我确信它可以很容易地适应另一个(它只是设置和时间*方法)。
  • @user3580294 您在上面看到的所有短条实际上都来自避免虚拟方法表查找。最短的一个(0.4 ns,即每个方法调用一个周期)只有在 JVM 知道只有一个实现并直接内联读取的字段时才有可能。第二个最短的(0.6 ns)还包含一个正确预测的分支测试,o 实际上是一个A 实例。第三个最短的(1.2 ns)来自内联A.myCode()B.myCode() 之间的切换。当桌子真正参与进来时,就会出现 10 倍的减速。
  • @user3580294 覆盖对性能的影响可能很大。 javaspecialists.eu/archive/Issue158.html@maaartinus 上的更多信息关于实际问题:hashCode 方法经过 JIT 的特殊处理。你可能会玩-XX:DisableIntrinsic=_hashCode,或者看看hg.openjdk.java.net/jdk8/jdk8/hotspot/file/a57a165b8296/src/…(和#l3977、#l4000、#l4103 ...搜索“hashCode”)。我无法指出原因,但猜测它隐藏在其中的某个地方......

标签: java performance hashcode


【解决方案1】:

hashCode 是在java.lang.Object 中定义的,所以在你自己的类中定义它根本没有多大用处。 (仍然是定义的方法,但没有区别)

JIT 有几种优化呼叫站点的方法(在本例中为 hashCode()):

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

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

请注意:调用任何扩展 Base 的类的 hashCode() 与调用 Object.hashCode() 相同,如果您在 Base 中添加显式 hashCode限制调用Base.hashCode()的潜在调用目标。

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

作为额外奖励:在加载新类时,JIT 必须对现有调用站点进行去优化。


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

【讨论】:

  • 这如何回答这个问题?显然,在您自己的类中定义 hashCode()确实 会产生影响,而且 JIT 显然不会像内联其他方法那样内联 hashCode()。请注意,对于每个测试,“正常”hashCode() 和自定义 myCode() 的实现数量完全相同。那么为什么看起来hashCode()myCode() 的内联方式不同呢?
  • 它确实回答了这个问题,这就是答案:简单地说,只有少数方法myCode() 和大量hashCode()。后者具有巨大的类层次结构,这会阻止优化——因为 JDK 本身中有大量定义 hashCode()。又一次没有对hashCode()进行特殊处理:toString()equals(Object)命运一模一样。
  • hashCode() 可能在很多类中实现,但是a)很多这些类没有加载,b)正如@laune 的回答中指出的那样,使用转发实现会删除所有hashCode()myCode() 之间的差异,这清楚地表明尽管有很多 hashCode()-implementing 类可用,但优化是可能的。此外,如果hashCode()-implementing 类的数量是一个因素,我预计hashCode() 的时间不会因预加载实现类而显着不同,b) 对于单个实现类而言是相同的。跨度>
  • 那么调用hashCode()的函数可以被内联尽管存在其他实现hashCode()的类,对吧?所以我没有看到你的答案如何适用于此。我正在使用 OP 使用的“预加载”——也就是说,使用代码使类加载器加载所有实现类,尽管只使用了其中的一些类。 exact 基准由 OP 提供,更改由 @laune 描述,您应该能够自己复制这个问题。如果您想要组装,请在原始帖子上发表评论。
  • @user3580294,我又添加了一段澄清通过 Base.hashCode() 调用的hashCode()——它们实际上是Object.hashCode(),因为Base 没有hashCode()。我认为这很清楚。所以在JDK和calipher框架之间会有很多定义hashCode()的类,所以调用是Object.v调用。如果您只有一个带有 hashCode 的类并且编译器可以将其视为 NOT java.lang.Object ,那么它显然是一个静态调用。这是这里的最后一条评论。
【解决方案2】:

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

【讨论】:

  • 我不确定......在这个错误中,案例“i_i”的性能很好,我在 Base 上调用 hashCode,而不是在 Object 上。但原因可能还是一样。
  • @maaartinus 如果您在Base 中定义hashCode,则不会出现性能回归。如果你没有在Base 中定义hashCode,那么Object.hashCode 将被调用,即使你写了((Base)b).hashCode()
【解决方案3】:

我可以确认调查结果。查看这些结果(省略重新编译):

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

【讨论】:

    【解决方案4】:

    我正在查看您的不变量以进行测试。它将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

    【讨论】:

      【解决方案5】:

      hashCode() 的语义比常规方法更复杂,因此 JVM 和 JIT 编译器在调用 hashCode() 时必须比调用常规虚拟方法时做更多的工作。

      一个特殊性对性能有负面影响:对空对象调用 hashCode() 是有效的并返回零。这需要比常规调用多一个分支,这本身就可以解释您所陈述的性能差异。

      请注意,由于引入了具有此语义的 Object.hashCode(target),这似乎仅来自 Java 7。想知道你在哪个版本上测试了这个问题,以及你是否会在 Java6 上也有同样的问题。

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

      E.

      【讨论】:

      • 你错了。 1. null.hashCode() 像任何其他方法一样抛出。 2. Objects 是一个实用类,不要与Object 混淆。 3. 不提供hashCode 意味着从Object.hashCode 继承,标记为native,但在内部(可能)委托给System.identityHashCode。 4.这不是JIT,它只是正常的继承。 5.在基准myCode同样继承自BaseBase.myCode 是抽象的,但这并没有改变
      • 在阅读了上面 Marco13 给出的链接上的 JVM 代码后,我相信我的回答是正确的。查看方法 inline_native_hashcode。
      • 我真的很想知道您从哪里提取了“在null 对象上调用hashCode() 是有效的”部分,因为任何理性的程序员都会说这会导致NPE。见here。它也在 JLS 中,第 15.12.4.4 节,定位要调用的方法:“如果目标引用为空,此时会抛出 NullPointerException。”
      • 此外,JIT 编译器可以像Object#hashCode() 这样的本机方法一样内联“正则编译”方法。如果有的话,刚刚返回 0 或其他一些常量的“正则编译”hashCode() 几乎可以保证比默认的 hashCode() 实现更快,尤其是在内联之后。 maaartinus 在所有方面都是正确的。如果您不这么认为,您将不得不更具体地了解您所看到的内容。
      • @EricNicolas 我也认为答案隐藏在hg.openjdk.java.net/jdk8/jdk8/hotspot/file/a57a165b8296/src/… 的“inline_native_hashcode”方法的深处——它谈到了“慢调用”、本机 hashCode 和 identityHashCode 以及 vtables 的内在函数——但我不明白它在那里实际做了什么,所以我没有尝试将其作为答案......
      猜你喜欢
      • 2017-01-18
      • 1970-01-01
      • 1970-01-01
      • 2011-05-30
      • 1970-01-01
      • 1970-01-01
      • 2011-03-23
      • 2012-01-06
      • 1970-01-01
      相关资源
      最近更新 更多