为了账号安全,请及时绑定邮箱和手机立即绑定

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

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

慕莱坞森 2023-06-08 17:27:18
我正在查看 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?:)
查看完整描述

1 回答

?
慕哥9229398

TA贡献1877条经验 获得超6个赞

您的问题的答案在于 JVM 规范,特别是您指出的不同之处:指令dup(JVMS §6.5.dup)。从那些文档:

复制操作数栈顶部的值并将复制的值压入操作数栈。

查看操作数堆栈文档(JVMS §2.6.2,重点添加):

少量 Java 虚拟机指令(dup指令 (§dup) 和swap(§swap))作为原始值在运行时数据区域上运行,而不考虑它们的特定类型;这些指令的定义方式使其不能用于修改或分解单个值。这些对操作数堆栈操作的限制是通过类文件验证(§4.10)强制执行的。

再深入一层,查看类验证部分(JVMS §4.10,重点添加):

链接时验证增强了运行时解释器的性能。可以消除在运行时为每条解释指令验证约束而必须执行的昂贵检查。Java 虚拟机可以假定这些检查已经执行

这表明这些限制是在链接时验证的,也就是 JVM 加载您的类文件时。所以回答你的问题:

使用这些结构背后的真正原因是什么?

让我们剖析一下指令在每种情况下的作用:

在第一种情况下(使用说明dup):

  1. invokevirtual将结果存储在操作数栈的顶部

  2. dup重复所以现在在堆栈顶部有两个结果副本

  3. astore_2将其存储到局部变量 #2 中,该变量从操作数堆栈中弹出一个引用

  4. ifnull检查操作数栈的顶部是否为空,如果是,则转到指令 15,否则继续(我们假设它不为空)

  5. aload_2将局部变量#2 推入操作数栈的顶部

  6. invokevirtual在操作数栈的顶部调用一个方法,弹出它,然后压入结果

  7. ireturn从操作数栈弹出顶部值并返回它

在第二种情况下:

  1. invokevirtual将结果存储在操作数栈的顶部

  2. astore_2将结果弹出操作数栈并将其存储在局部变量 #2 中

  3. aload_2将局部变量#2 推入操作数栈的顶部

  4. ifnull检查操作数栈的顶部是否为空,如果是,则转到指令 15,否则继续(我们假设它不为空)

  5. aload_2将局部变量#2 推入操作数栈的顶部

  6. invokevirtual在操作数栈的顶部调用一个方法,弹出它,然后压入结果

  7. ireturn从操作数栈弹出顶部值并返回它

那么有什么区别呢?第一个调用aload_2一次又一次dup,第二个只调用aload两次。这里的区别几乎没有。如果查看整个操作过程中堆栈的大小,您会发现第一个实现将操作数堆栈增加了一个额外的值(少于 10 个字节,通常为 8 或 4 个字节,具体取决于 64 位或 32 位 JVM ), 但从堆栈内存中加载的局部变量少了一个。第二个使操作数堆栈稍微小一些,但有一个额外的局部变量加载(读取:从内存中获取)。

归根结底,这些优化的影响非常小,除非是在内存极低的应用程序中,例如嵌入式系统。那么对你来说?做可读的事情。

如有疑问:“过早优化(可能)是万恶之源。” 除非您知道您的代码很慢或者可以在运行之前证明它很慢,否则最好编写可读的代码。这几乎不属于您应该提前优化的关键 3%。


查看完整回答
反对 回复 2023-06-08
  • 1 回答
  • 0 关注
  • 109 浏览

添加回答

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信