【问题标题】:Why does the Oracle Java compiler prefer the no-args StringBuilder constructor?为什么 Oracle Java 编译器更喜欢无参数 StringBuilder 构造函数?
【发布时间】:2014-05-29 13:54:29
【问题描述】:

纯粹出于兴趣,我一直在研究 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 的编译器不做同样的优化?


基于 cmets,我使用更长的 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 " ";
    }
}

除了字面量的内容略有不同之外,生成的字节码完全相同,仍然使用无参数构造函数来实例化StringBuilder,然后再附加到它。在这种情况下,据我所知,代码的参数构造函数版本应该胜过无参数版本。这是因为需要在第一次调用append() 时重新调整支持char[] 的大小,然后可能需要在下一个append() 上再次执行此操作,如果附加了@987654336 @ 特别大。


根据 AnubianNoob 的建议,我对 System.arraycopy(...) 进行了快速性能测试,看看它是否确实针对空数组进行了优化。这是使用的代码:

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");
    }
}

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

Elapsed Time (empty array copies)
=================================
26660199ns
Elapsed Time (non-empty array copies)
=====================================
19431962ns

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

【问题讨论】:

  • 您为什么认为这是一种优化?两者唯一的区别是容量,使用基于字符串的构造函数时容量更大。
  • @Duncan 我并不是说一个版本一定比另一个更好(我还没有深入研究以形成一个明智的意见),我只是想知道为什么 Oracle当 ECJ 更喜欢有参数的构造函数时,更喜欢无参数的构造函数。如果我要手动编写代码,我会使用带参数的构造函数,所以我(也许天真地)假设编译器也会这样做。
  • @Duncan 仔细观察后,如果附加到 StringBuilder 的初始 String 长度超过 16 个字符,那么后面的 char[] 将立即扩展为至少 34,如果String 少于34 个字符,则有一些备用容量。如果它长于 34 个字符,那么新数组将恰好足够长以适应新内容,这意味着对 append() 的下一次调用将再次导致数组调整大小。鉴于此,我认为参数化构造函数将是编译器选择的更好选择...
  • ...因为它确保它足够大以容纳最初的String,并在最后剩余 16 个字符的备用容量。
  • 恐怕这不是一个有意义的基准。使用 Java 进行基准测试是一项棘手的工作,您必须考虑诸如延迟 JIT 之类的问题。如果第一个测试用例一直运行得较慢,我敢打赌你吃午饭,如果你交换测试的顺序,结果会改变:)。

标签: java javac bytecode compiler-optimization


【解决方案1】:

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

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

大多数人认为编译器是由超级人类设计的魔法程序,它们总是做最好的事情。但这不是真的,编译器也是由通常的程序员编写的,他们并不总是考虑几个小时来编译任何特定事物的最佳方法。他们的日程安排很紧,需要完成一些功能,因此最简单的解决方案通常是首选。

【讨论】:

  • 这可能是原因。也许并非偶然,如果您始终使用相同的模式,那么在运行时优化操作也会变得更容易。
  • 我确实很好奇为什么没有接受多个参数或String[] 的String 构造函数。给定带有 String[] 参数的 String 构造函数,构造和填充 String[] 并将其传递给该构造函数可能比在每种情况下都使用 StringBuilder 更快(它只需要构造一个临时对象) .
【解决方案2】:

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

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

【讨论】:

  • 所以,你说没有区别,Oracle 只是选择一个随机的解决方案。对吗?
  • @Val 可能吗?让容量“自然”扩展而不是直接设置,或者 JIT 编译器的某种编写方式可能有一些原因,但我不知道更具体的原因。
  • 如果不知道怎么回答?该问题询问您不知道的内容。
  • 因为我可以提供一个合理的猜测,我想?不幸的是,只有真正编写了处理此问题的编译器代码的人才能给出权威的答案。
【解决方案3】:

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

【讨论】:

  • 您的意思是发现空的StringBuilder 构造函数调用可能更容易吗?
  • 它可能不是更容易发现一个空的构造函数调用,但如果你只需要寻找一个模式,它会简化模式识别。编译器不能在所有情况下使用参数化构造函数(例如,对于1 + "one"),但它可以在所有情况下使用空构造函数。
  • @MikeStrobel 这确实很有意义,我没有这样想过。感谢您的洞察力。
【解决方案4】:

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

考虑:

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

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

【讨论】:

  • 我同意这个特定示例中的速度是相同的(在我自己的示例中也是如此),但如果您的初始 String 超过 16 个字符,则会导致 AbstractStringBuilder重新分配其char[] 以保存String,如果使用参数化构造函数,则不需要。
  • @JonK 实际上,如果您使用长度超过 16 个字符的字符串会发生什么?
  • append() 首先使用输入String 的长度作为参数调用AbstractStringBuilder#ensureCapacity(int)。无参数构造函数将初始容量设置为 16,因此如果 String 比这更长,那么它将调用 expandCapacity(int) 并将数组的容量增加到 34 或 String 的长度,以较大者为准。还是您的意思是生成的字节码?
  • 我刚刚用更长的String 进行了快速测试,生成的字节码是相同的(明显的例外是String 的内容不同) - 它仍然使用 no- args 构造函数。我会相应地更新问题。
  • @JonK 这很有趣... arrayCopy 会针对空数组进行优化吗?这可能值得分析...
【解决方案5】:

顺便说一句,JDK issue tracker 中有相关的问题:JDK-4059189 和相关的。最初的提案日期为 1997 年!那里没有太多讨论。这意味着这个问题要么被认为不重要,要么被 JIT 优化。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-12-22
    • 2018-05-26
    • 1970-01-01
    • 1970-01-01
    • 2018-04-02
    • 2021-08-12
    相关资源
    最近更新 更多