java - 为什么jdk代码样式使用变量分配并在同一行上读取-例如(i = 2)<最大值

标签 java java-collections-api

我注意到,在jdk源代码中,更具体地说,在collections框架中,在将变量读入表达式之前,会优先分配变量。这只是一个简单的偏好还是我不知道的更重要的东西?我能想到的一个原因是变量只在这个表达式中使用。
因为我不习惯这种风格,所以我觉得很难读。代码非常简洁。下面您可以看到一个取自java.util.HashMap.getNode()

Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 && ...) {
   ...
}

最佳答案

正如评论中已经提到的:doug lea是collections框架和并发包的主要作者之一,他倾向于做一些优化,对于普通人来说,这些优化可能看起来很混乱(甚至是违反直觉)。
这里有一个“著名”的例子是copying fields to local variables以最小化字节码的大小,这实际上也是用您提到的例子中的table字段和局部tab变量来实现的!
对于非常简单的测试,访问是否“内联”似乎没有什么区别(指的是生成的字节码大小)。因此,我试图创建一个与您提到的getNode方法的结构大致相似的示例:对作为数组的字段的访问、长度检查、对一个数组元素的字段的访问……
testSeparate方法执行单独的赋值和检查
testInlined方法使用if样式中的赋值
testRepeated方法(作为反例)重复执行每个访问
代码:

class Node
{
    int k;
    int j;
}

public class AssignAndUseTestComplex
{
    public static void main(String[] args)
    {
        AssignAndUseTestComplex t = new AssignAndUseTestComplex();
        t.testSeparate(1);
        t.testInlined(1);
        t.testRepeated(1);
    }

    private Node table[] = new Node[] { new Node() };

    int testSeparate(int value)
    {
        Node[] tab = table;
        if (tab != null)
        {
            int n = tab.length;
            if (n > 0)
            {
                Node first = tab[(n-1)];
                if (first != null)
                {
                    return first.k+first.j;
                }
            }
        } 
        return 0;
    }

    int testInlined(int value)
    {
        Node[] tab; Node first, e; int n;
        if ((tab = table) != null && (n = tab.length) > 0 && 
            (first = tab[(n - 1)]) != null) {
            return first.k+first.j;
        }
        return 0;
    }

    int testRepeated(int value)
    {
        if (table != null)
        {
            if (table.length > 0)
            {
                if (table[(table.length-1)] != null)
                {
                    return table[(table.length-1)].k+table[(table.length-1)].j;
                }
            }
        } 
        return 0;
    }

}

以及由此产生的字节码:testSeparate方法使用41条指令:
  int testSeparate(int);
    Code:
       0: aload_0
       1: getfield      #15                 // Field table:[Lstackoverflow/Node;
       4: astore_2
       5: aload_2
       6: ifnull        40
       9: aload_2
      10: arraylength
      11: istore_3
      12: iload_3
      13: ifle          40
      16: aload_2
      17: iload_3
      18: iconst_1
      19: isub
      20: aaload
      21: astore        4
      23: aload         4
      25: ifnull        40
      28: aload         4
      30: getfield      #37                 // Field stackoverflow/Node.k:I
      33: aload         4
      35: getfield      #41                 // Field stackoverflow/Node.j:I
      38: iadd
      39: ireturn
      40: iconst_0
      41: ireturn

testInlined方法确实小了一点,有39条指令
  int testInlined(int);
    Code:
       0: aload_0
       1: getfield      #15                 // Field table:[Lstackoverflow/Node;
       4: dup
       5: astore_2
       6: ifnull        38
       9: aload_2
      10: arraylength
      11: dup
      12: istore        5
      14: ifle          38
      17: aload_2
      18: iload         5
      20: iconst_1
      21: isub
      22: aaload
      23: dup
      24: astore_3
      25: ifnull        38
      28: aload_3
      29: getfield      #37                 // Field stackoverflow/Node.k:I
      32: aload_3
      33: getfield      #41                 // Field stackoverflow/Node.j:I
      36: iadd
      37: ireturn
      38: iconst_0
      39: ireturn

最后,testRepeated方法使用了多达63条指令
  int testRepeated(int);
    Code:
       0: aload_0
       1: getfield      #15                 // Field table:[Lstackoverflow/Node;
       4: ifnull        62
       7: aload_0
       8: getfield      #15                 // Field table:[Lstackoverflow/Node;
      11: arraylength
      12: ifle          62
      15: aload_0
      16: getfield      #15                 // Field table:[Lstackoverflow/Node;
      19: aload_0
      20: getfield      #15                 // Field table:[Lstackoverflow/Node;
      23: arraylength
      24: iconst_1
      25: isub
      26: aaload
      27: ifnull        62
      30: aload_0
      31: getfield      #15                 // Field table:[Lstackoverflow/Node;
      34: aload_0
      35: getfield      #15                 // Field table:[Lstackoverflow/Node;
      38: arraylength
      39: iconst_1
      40: isub
      41: aaload
      42: getfield      #37                 // Field stackoverflow/Node.k:I
      45: aload_0
      46: getfield      #15                 // Field table:[Lstackoverflow/Node;
      49: aload_0
      50: getfield      #15                 // Field table:[Lstackoverflow/Node;
      53: arraylength
      54: iconst_1
      55: isub
      56: aaload
      57: getfield      #41                 // Field stackoverflow/Node.j:I
      60: iadd
      61: ireturn
      62: iconst_0
      63: ireturn

因此,这种“晦涩”的查询和赋值方式似乎确实可以节省几个字节的字节码,而且(考虑到链接答案中关于将字段存储在局部变量中的理由),这可能是使用这种方式的原因。
但是…
无论如何:在方法执行了几次之后,jit将启动,生成的机器代码将与原始字节码“毫无关系”——我非常确定,最终所有三个版本都将编译为相同的机器代码。
所以底线是:不要使用这种风格。相反,只要write dumb code这是容易阅读和维护的。你会知道什么时候该用这样的“优化”了。
编辑:一个简短的附录…
我做了进一步的测试,并比较了与jit生成的实际机器代码相关的testSeparatetestInlined方法。
我对main方法进行了一些修改,以防止不现实的过度优化或jit可能采用的其他快捷方式,但是实际的方法没有被修改。
正如所料:当使用热点反汇编jvm和-XX:+UnlockDiagnosticVMOptions -XX:+LogCompilation -XX:+PrintAssembly调用方法几千次时,这两个方法的实际机器代码是相同的。
因此,jit再一次很好地完成了它的工作,程序员可以专注于编写可读的代码(不管这意味着什么)。
…以及轻微的更正/澄清:
我没有测试第三个方法testRepeated,因为它不等同于其他方法(因此,它不能产生相同的机器代码)。顺便说一下,这是将字段存储在局部变量中的策略的另一个小优势:它提供了一种(非常有限,但有时很方便)形式的“线程安全”:它确保在执行方法时数组的长度(如tab方法中的getNode数组)不会更改。

本文翻译自 https://stackoverflow.com/questions/28975415/

网站遵循 CC BY-SA 4.0 协议,转载或引用请注明出处。


相关文章:

java - 在JPA表上更新UniqueConstrains

java - 与Arrays.asList构造的列表的列表差异具有令人惊讶的结果。为什么? [重复]

java - 正则表达式句子拆分

java - Freemarker无法访问对象字段

java - 如何重写哈希码和equals方法,以避免在Java的HashSet中添加重复的字符串?

java - 使用Java 8集合流API进行堆栈

java - 按原始布尔类型对ArrayList进行排序

java - 从列表中删除另一个列表中不存在的所有对象

java - 团结| Android-无法签名APK Packag:不支持的major.minor版本52.0

java - 如何在另一个JPanel内的JPanel上绘制Graphics2D?