有 Java 编程相关的问题?

你可以在下面搜索框中键入要查询的问题!

java为什么哈希代码比类似的方法慢?

通常,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,而且可能会有很多帮助

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不能像其他方法那样优化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再次具有竞争力

results2

更新3

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

因此,这看起来像是错过了一次优化机会

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

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


共 (5) 个答案

  1. # 2 楼答案

    我可以证实调查结果。见这些结果(省略重新编译):

    $ /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字段

    现在,有趣的部分是:如果将以下方法添加到类库中

    @Override
    public int hashCode(){
        return super.hashCode();
    }
    

    hashCode的执行时间与overCode的执行时间不再不同

    基地。爪哇:

    public class Base {
    private int code;
    public Base( int x ){
        code = x;
    }
    public int overCode(){
    return code;
    }
    }
    

    苏巴。爪哇:

    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();
    }
    }
    
  2. # 3 楼答案

    hashCode是在java.lang.Object中定义的,因此在您自己的类中定义它根本没有什么作用。(这仍然是一个已定义的方法,但没有区别)

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

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

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

    请注意:调用任何扩展基类的hashCode()与调用Object.hashCode()是一样的,如果在基类中添加显式hashCode,将限制调用Base.hashCode()的潜在调用目标,那么它在字节码中的编译方式就是这样的

    太多的类(在JDK本身中)被hashCode()重写,因此在非内联HashMap类似结构的情况下,调用是通过vtable执行的,即慢

    额外的好处是:在加载新类时,JIT必须对现有呼叫站点进行去优化


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

  3. # 4 楼答案

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

    一种特殊性对性能有负面影响:对null对象调用hashCode()是有效的,并且返回零。这需要比常规调用多进行一次分支,而常规调用本身就可以解释性能差异

    请注意,由于引入了对象,这似乎只适用于Java 7。具有此语义的哈希代码(目标)。了解您在哪个版本上测试这个问题,以及您是否会在Java6上测试这个问题,这将是一件有趣的事情

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

    E

  4. # 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