为什么Oracle Java编译器更喜欢no-args StringBuilder构造函数?

时间:2014-05-29 13:54:29

标签: java javac bytecode compiler-optimization

纯粹出于兴趣,我一直在研究Oracle Java编译器如何处理String串联,并且我看到了一些我没想到的东西。

给出以下代码:

public class StringTest {
    public static void main(String... args) {
        String s = "Test" + getSpace() + "String.";
        System.out.println(s.toString());
    }

    // Stops the compiler optimising the concatenations down to a
    // single string literal.
    static String getSpace() {
        return " ";
    }
}

我期望编译器将其优化为相当于:

String s = new StringBuilder("Test").append(getSpace())
                   .append("String.").toString();

但它实际上编译为相当于:

String s = new StringBuilder().append("Test").append(getSpace())
                   .append("String.").toString();

我正在使用32位jdk1.7.0_55版本编译它。这是javap -v -l

的输出
public class StringTest
  SourceFile: "StringTest.java"
  minor version: 0
  major version: 51
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #14.#25        //  java/lang/Object."<init>":()V
   #2 = Class              #26            //  java/lang/StringBuilder
   #3 = Methodref          #2.#25         //  java/lang/StringBuilder."<init>":()V
   #4 = String             #27            //  Test
   #5 = Methodref          #2.#28         //  java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   #6 = Methodref          #13.#29        //  StringTest.getSpace:()Ljava/lang/String;
   #7 = String             #30            //  String.
   #8 = Methodref          #2.#31         //  java/lang/StringBuilder.toString:()Ljava/lang/String;
   #9 = Fieldref           #32.#33        //  java/lang/System.out:Ljava/io/PrintStream;
  #10 = Methodref          #34.#31        //  java/lang/String.toString:()Ljava/lang/String;
  #11 = Methodref          #35.#36        //  java/io/PrintStream.println:(Ljava/lang/String;)V
  #12 = String             #37            //
  #13 = Class              #38            //  StringTest
  #14 = Class              #39            //  java/lang/Object
  #15 = Utf8               <init>
  #16 = Utf8               ()V
  #17 = Utf8               Code
  #18 = Utf8               LineNumberTable
  #19 = Utf8               main
  #20 = Utf8               ([Ljava/lang/String;)V
  #21 = Utf8               getSpace
  #22 = Utf8               ()Ljava/lang/String;
  #23 = Utf8               SourceFile
  #24 = Utf8               StringTest.java
  #25 = NameAndType        #15:#16        //  "<init>":()V
  #26 = Utf8               java/lang/StringBuilder
  #27 = Utf8               Test
  #28 = NameAndType        #40:#41        //  append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #29 = NameAndType        #21:#22        //  getSpace:()Ljava/lang/String;
  #30 = Utf8               String.
  #31 = NameAndType        #42:#22        //  toString:()Ljava/lang/String;
  #32 = Class              #43            //  java/lang/System
  #33 = NameAndType        #44:#45        //  out:Ljava/io/PrintStream;
  #34 = Class              #46            //  java/lang/String
  #35 = Class              #47            //  java/io/PrintStream
  #36 = NameAndType        #48:#49        //  println:(Ljava/lang/String;)V
  #37 = Utf8
  #38 = Utf8               StringTest
  #39 = Utf8               java/lang/Object
  #40 = Utf8               append
  #41 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
  #42 = Utf8               toString
  #43 = Utf8               java/lang/System
  #44 = Utf8               out
  #45 = Utf8               Ljava/io/PrintStream;
  #46 = Utf8               java/lang/String
  #47 = Utf8               java/io/PrintStream
  #48 = Utf8               println
  #49 = Utf8               (Ljava/lang/String;)V
{
  public StringTest();
    flags: ACC_PUBLIC
    LineNumberTable:
      line 2: 0
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 2: 0

  public static void main(java.lang.String...);
    flags: ACC_PUBLIC, ACC_STATIC, ACC_VARARGS
    LineNumberTable:
      line 4: 0
      line 5: 27
      line 6: 37
    Code:
      stack=2, locals=2, args_size=1
         0: new           #2                  // class java/lang/StringBuilder
         3: dup
         4: invokespecial #3                  // Method java/lang/StringBuilder."<init>":()V
         7: ldc           #4                  // String Test
         9: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        12: invokestatic  #6                  // Method getSpace:()Ljava/lang/String;
        15: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        18: ldc           #7                  // String String.
        20: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        23: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        26: astore_1
        27: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;
        30: aload_1
        31: invokevirtual #10                 // Method java/lang/String.toString:()Ljava/lang/String;
        34: invokevirtual #11                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        37: return
      LineNumberTable:
        line 4: 0
        line 5: 27
        line 6: 37

  static java.lang.String getSpace();
    flags: ACC_STATIC
    LineNumberTable:
      line 10: 0
    Code:
      stack=1, locals=0, args_size=0
         0: ldc           #12                 // String
         2: areturn
      LineNumberTable:
        line 10: 0
}

有趣的是,我已经读过here ECJ编译器确实实际编译到争论的构造函数(尽管我还没有为自己验证过),所以我的问题是为什么Oracle的编译器没有进行相同的优化


根据评论,我使用较长的String进行了另一项测试,以便立即超过StringBuilder支持char[]的默认长度:

public class StringTest {
    public static void main(String... args) {
        String s = "Testing a much, much longer " + getSpace() + "String.";
        System.out.println(s.toString());
    }

    // Stops the compiler optimising the concatenations down to a single string literal
    static String getSpace() {
        return " ";
    }
}

除了文字内容略有不同外,生成的字节码完全相同,仍然使用no-args构造函数在附加StringBuilder之前对其进行实例化。在这种情况下,代码的有争议的构造函数应该超出我的判断。这是因为需要在第一次调用char[]时重新调整后备append()的大小,然后可能需要在下一次{{1}时再次执行此操作如果附加的append()特别大。


在AnubianNoob的建议中,我对String进行了快速性能测试,看看它是否确实针对空数组进行了优化。这是使用的代码:

System.arraycopy(...)

在带有i7-2600 CPU @ 3.40 GHz 3.39 GHz和3.24 GB可用RAM的Windows 7.1 32位计算机上运行:

public class ArrayCopyTest {
    public static void main(String... args) {

        char[] array = new char[16];
        final long test1Start = System.nanoTime();
        for (int i = 0; i < 1000000; i++) {
            System.arraycopy(array, 0, array, 0, array.length);
        }

        final long test1End = System.nanoTime();
        System.out.println("Elapsed Time (empty array copies)");
        System.out.println("=================================");
        System.out.println((test1End - test1Start) + "ns");

        char[] array2 = new char[] {'0', '1', '2', '3', '4', '5', '6', '7', '8',
             '9', 'a', 'b', 'c', 'd', 'e', 'f'};

        final long test2Start = System.nanoTime();
        for (int i = 0; i < 1000000; i++) {
            System.arraycopy(array2, 0, array2, 0, array2.length);
        }

        final long test2End = System.nanoTime();
        System.out.println("Elapsed Time (non-empty array copies)");
        System.out.println("=====================================");
        System.out.println((test2End - test2Start) + "ns");
    }
}

为了确定,我跑了大约五次。实际上,当数组没有空时,它在一百万次迭代中表现更好。正如Mike Strobel正确指出的那样,上述并不是一个有意义的基准。 / p>

5 个答案:

答案 0 :(得分:4)

可能是因为String构造函数无论如何调用了append()

public StringBuilder(String str) {
    super(str.length() + 16);
    append(str);
}

答案 1 :(得分:4)

我认为这只是懒惰。为什么?因为如果你选择了arg-constructor,你需要进一步检查。你必须检查要连接的第一个表达式是否是一个字符串,如果是,你可以使用arg构造函数,否则,你必须回退到no-arg构造函数。这比简单地总是使用no-arg构造函数要多得多。

如果我是那个编译器开发人员,我也会选择简单的方法,因为隐式字符串连接肯定不是许多应用程序中的瓶颈,而且差别很小,以至于不值得麻烦。

大多数人认为编译器是由超级人类设计的魔术程序,总能做到最好的东西。但事实并非如此,编译器也是由通常的程序员编写的,他们并不总是在思考什么是 编译任何特定事物的最佳方法。他们有紧迫的时间表和需要完成的功能,因此最简单的解决方案往往是首选解决方案。

答案 2 :(得分:1)

这可能是因为JVM优化了字符串连接,它可能更好地识别字节码中的字符串连接模式,就像现在实现它一样。

答案 3 :(得分:0)

正如另一个人所提到的那样,StringBuilder类在其构造函数中调用append(),并且自己追加它会更具可读性和一致性。

考虑:

new StringBuilder("Hello").append("World");
new StringBuilder().append("Hello").append("World");

这可能不是最好的例子,但是两个附加内容比将其传递给构造函数要简单得多。速度是一样的。

答案 4 :(得分:0)

顺便说一句,JDK问题跟踪器中存在相关问题:JDK-4059189和相关问题。最初的提案是1997年的日期!那里没有太多的讨论。这意味着这个问题被认为是不重要的,或者这个案例是由JIT优化的。

相关问题