JDK 中的非常规代码 - 用于未知原因的特定构造

我正在查看 JDK(JDK 12,但它也适用于旧版本)代码并发现了一些奇怪的结构,我不明白为什么要使用它们。让我们举个例子Map.computeIfPresent,因为它很简单:


default V computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {

    Object oldValue;

    if ((oldValue = this.get(key)) != null) {

        V newValue = remappingFunction.apply(key, oldValue);

        if (newValue != null) {

            this.put(key, newValue);

            return newValue;

        } else {

            this.remove(key);

            return null;

        }

    } else {

        return null;

    }

}

这个结构if ((oldValue = this.get(key)) != null)让我很吃惊。我知道这是可能的,因为它没什么特别的,但在正常的生产代码中我会认为它是一种代码味道。为什么不直接写成正常的方式(Object oldValue = this.get(key))?一定是一些离合器优化,这就是我的想法。


写了一个较小的版本来检查字节码:


int computeIfPresent(int key) {

  Integer oldValue;

  if ((oldValue = get(key)) != null) {

    return oldValue;

  } else {

    return 2;

  }

}

字节码输出:


int computeIfPresent(int);

  Code:

     0: aload_0

     1: iload_1

     2: invokevirtual #2                  // Method get:(I)Ljava/lang/Integer;

     5: dup

     6: astore_2

     7: ifnull        15

    10: aload_2

    11: invokevirtual #3                  // Method java/lang/Integer.intValue:()I

    14: ireturn

    15: iconst_2

    16: ireturn

具有经典变量初始化的“普通”版本的字节码:


int computeIfPresent(int);

  Code:

     0: aload_0

     1: iload_1

     2: invokevirtual #2                  // Method get:(I)Ljava/lang/Integer;

     5: astore_2

     6: aload_2

     7: ifnull        15

    10: aload_2

    11: invokevirtual #3                  // Method java/lang/Integer.intValue:()I

    14: ireturn

    15: iconst_2

    16: ireturn

唯一的区别是dup + astore_2vs。astore_2 + aload_2我什至怀疑第一个“离合器优化”版本更糟,因为dup它被使用并且堆栈无缘无故地更大。也许我的例子太简单了,优化在更复杂的上下文中扩展了很多。


这是一个简单的例子,绝对不是 JDK 代码中的一个例子——打开HashMap.java,有很多这样的片段,有时在同一行有多个:


if ((first = tab[i = (n - 1) & hash]) != null)


尽管它真的很简单,但由于这些结构,我不得不停下来想一想这段代码实际上做了什么。


使用这些结构背后的真正原因是什么?我敢肯定这不仅仅是糟糕的代码。在我看来,代码质量受到很大影响,因此收益一定是可观的。或者只是规则leave small optimizations to JIT不适用于 JDK,因为它必须尽可能多地压缩性能?


或者这只是规则的极端initialize variables as late as possible?:)


慕莱坞森
浏览 110回答 1
1回答

慕哥9229398

您的问题的答案在于 JVM 规范,特别是您指出的不同之处:指令dup(JVMS §6.5.dup)。从那些文档:复制操作数栈顶部的值并将复制的值压入操作数栈。查看操作数堆栈文档(JVMS §2.6.2,重点添加):少量 Java 虚拟机指令(dup指令 (§dup) 和swap(§swap))作为原始值在运行时数据区域上运行,而不考虑它们的特定类型;这些指令的定义方式使其不能用于修改或分解单个值。这些对操作数堆栈操作的限制是通过类文件验证(§4.10)强制执行的。再深入一层,查看类验证部分(JVMS §4.10,重点添加):链接时验证增强了运行时解释器的性能。可以消除在运行时为每条解释指令验证约束而必须执行的昂贵检查。Java 虚拟机可以假定这些检查已经执行。这表明这些限制是在链接时验证的,也就是 JVM 加载您的类文件时。所以回答你的问题:使用这些结构背后的真正原因是什么?让我们剖析一下指令在每种情况下的作用:在第一种情况下(使用说明dup):invokevirtual将结果存储在操作数栈的顶部dup重复所以现在在堆栈顶部有两个结果副本astore_2将其存储到局部变量 #2 中,该变量从操作数堆栈中弹出一个引用ifnull检查操作数栈的顶部是否为空,如果是,则转到指令 15,否则继续(我们假设它不为空)aload_2将局部变量#2 推入操作数栈的顶部invokevirtual在操作数栈的顶部调用一个方法,弹出它,然后压入结果ireturn从操作数栈弹出顶部值并返回它在第二种情况下:invokevirtual将结果存储在操作数栈的顶部astore_2将结果弹出操作数栈并将其存储在局部变量 #2 中aload_2将局部变量#2 推入操作数栈的顶部ifnull检查操作数栈的顶部是否为空,如果是,则转到指令 15,否则继续(我们假设它不为空)aload_2将局部变量#2 推入操作数栈的顶部invokevirtual在操作数栈的顶部调用一个方法,弹出它,然后压入结果ireturn从操作数栈弹出顶部值并返回它那么有什么区别呢?第一个调用aload_2一次又一次dup,第二个只调用aload两次。这里的区别几乎没有。如果查看整个操作过程中堆栈的大小,您会发现第一个实现将操作数堆栈增加了一个额外的值(少于 10 个字节,通常为 8 或 4 个字节,具体取决于 64 位或 32 位 JVM ), 但从堆栈内存中加载的局部变量少了一个。第二个使操作数堆栈稍微小一些,但有一个额外的局部变量加载(读取:从内存中获取)。归根结底,这些优化的影响非常小,除非是在内存极低的应用程序中,例如嵌入式系统。那么对你来说?做可读的事情。如有疑问:“过早优化(可能)是万恶之源。” 除非您知道您的代码很慢或者可以在运行之前证明它很慢,否则最好编写可读的代码。这几乎不属于您应该提前优化的关键 3%。
打开App,查看更多内容
随时随地看视频慕课网APP

相关分类

Java