3

我注意到在 jdk 源代码中,更具体地说,在集合框架中,在表达式中读取变量之前,优先分配变量。这只是一个简单的偏好还是我不知道的更重要的东西?我能想到的一个原因是该变量仅在此表达式中使用。

由于我不习惯这种风格,我觉得很难阅读。代码非常简洁。下面你可以看到一个例子取自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 && ...) {
   ...
}
4

1 回答 1

7

正如评论中已经提到的:Doug Lea 是集合框架和并发包的主要作者之一,他倾向于做一些对于普通人来说可能看起来令人困惑(甚至违反直觉)的优化。

这里的一个“著名”示例是将字段复制到局部变量以最小化字节码的大小,这实际上也是在您提到的示例中使用table字段和局部变量完成的!tab


对于非常简单的测试,访问是否“内联”似乎没有区别(指的是生成的字节码大小)。因此,我尝试创建一个与getNode您提到的方法的结构大致相似的示例:访问作为数组的字段、长度检查、访问一个数组元素的字段......

  • testSeparate方法将分配和检查分开
  • testInlined方法使用 assignment-in-if-style
  • 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 将启动,生成的机器代码将与原始字节码“无关” - 我很确定所有三个版本实际上都是最后编译成相同的机器码。

所以底线是:不要使用这种风格。相反,只需编写易于阅读和维护的愚蠢代码。你会知道什么时候轮到你使用这些“优化”。


编辑:一个简短的附录......

我做了进一步的测试,并比较了 JIT 生成的实际机器代码testSeparate的方法和方法。testInlined

main我稍微修改了该方法,以防止 JIT 可能采用的不切实际的过度优化或其他捷径,但实际方法并未修改。

正如预期的那样:当使用热点反汇编 JVM 和 调用方法几千次时-XX:+UnlockDiagnosticVMOptions -XX:+LogCompilation -XX:+PrintAssembly,两种方法的实际机器代码是相同的。

因此,JIT 再次完成了它的工作,程序员可以专注于编写可读代码(无论这意味着什么)。

...以及一个小的更正/澄清:

我没有测试第三种方法,testRepeated因为它不等同于其他方法(因此,它不能产生相同的机器代码)。顺便说一句,这是将字段存储在局部变量中的策略的另一个小优点:它提供了一种(非常有限,但有时很方便)形式的“线程安全”:确保数组的长度(如) 方法中的tab数组在getNode方法HashMap执行时不能更改。

于 2015-03-10T23:40:39.603 回答