Javac的StringBuilder优化弊大于利吗?

时间:2014-07-07 15:54:09

标签: java string optimization javac stringbuilder

我们说我们有一些代码如下:

public static void main(String[] args) {
    String s = "";
    for(int i=0 ; i<10000 ; i++) {
        s += "really ";
    }
    s += "long string.";
}

(是的,我知道一个更好的实现会使用StringBuilder,但请耐心等待。)

平凡地说,我们可能期望生成的字节码类似于以下内容:

public static void main(java.lang.String[]);
Code:
   0: ldc           #2                  // String 
   2: astore_1      
   3: iconst_0      
   4: istore_2      
   5: iload_2       
   6: sipush        10000
   9: if_icmpge     25
  12: aload_1       
  13: ldc           #3                  // String really 
  15: invokevirtual #4                  // Method java/lang/String.concat:(Ljava/lang/String;)Ljava/lang/String;
  18: astore_1      
  19: iinc          2, 1
  22: goto          5
  25: aload_1       
  26: ldc           #5                  // String long string.
  28: invokevirtual #4                  // Method java/lang/String.concat:(Ljava/lang/String;)Ljava/lang/String;
  31: astore_1      
  32: return

然而,相反,编译器试图变得更聪明 - 而不是使用concat方法,它在优化中使用StringBuilder对象,所以我们得到以下内容:

public static void main(java.lang.String[]);
Code:
   0: ldc           #2                  // String 
   2: astore_1      
   3: iconst_0      
   4: istore_2      
   5: iload_2       
   6: sipush        10000
   9: if_icmpge     38
  12: new           #3                  // class java/lang/StringBuilder
  15: dup           
  16: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
  19: aload_1       
  20: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  23: ldc           #6                  // String really 
  25: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  28: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
  31: astore_1      
  32: iinc          2, 1
  35: goto          5
  38: new           #3                  // class java/lang/StringBuilder
  41: dup           
  42: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
  45: aload_1       
  46: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  49: ldc           #8                  // String long string.
  51: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  54: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
  57: astore_1      
  58: return

但是,这对我来说似乎相反 - 而不是在整个循环中使用一个字符串构建器,而是为每个单个连接操作创建一个,使其等效于以下内容:

public static void main(String[] args) {
    String s = "";
    for(int i=0 ; i<10000 ; i++) {
        s = new StringBuilder().append(s).append("really ").toString();
    }
    s = new StringBuilder().append(s).append("long string.").toString();
}

现在,而不是仅仅创建大量字符串对象并抛弃它们的原始琐碎的错误的方法,编译器已经产生了一个更糟糕的方法创建大量String个对象,大量StringBuilder个对象,调用更多方法,然后仍将它们全部抛弃以生成与没有优化的情况相同的输出。

所以问题必须是 - 为什么?我理解在这种情况下:

String s = getString1() + getString2() + getString3();

...编译器将为所有三个字符串创建一个StringBuilder对象,因此在某些情况下优化很有用。但是,检查字节码表明甚至将上述情况分为以下几种情况:

String s = getString1();
s += getString2();
s += getString3();

...意味着我们回过头来分别创建了三个StringBuilder个对象。我理解这些是否是奇怪的角落情况,但以这种方式(并在循环中)附加到字符串是非常常见的操作。

当然,在编译时确定编译器生成的StringBuilder是否只附加了一个值 - 如果是这种情况,那么请使用简单的concat操作吗?

这完全是8u5(但是,至少可以追溯到Java 5,可能之前。)FWIW,我的基准测试(不出所料)使手动concat()方法比使用+=快2倍3倍在一个包含10,000个元素的循环中。当然,使用手册StringBuilder始终是首选方法,但编译器肯定不会对+=方法的性能产生负面影响吗?

2 个答案:

答案 0 :(得分:6)

  

所以问题必须是 - 为什么?

目前尚不清楚为什么他们在字节码编译器中没有更好地优化它。您需要询问Oracle Java编译器团队。

一种可能的解释是,HotSpot JIT编译器中可能存在将字节码序列优化为更好的代码。 (如果你很好奇,你可以修改代码,以便它编译JIT ......然后捕获并检查本机代码。但是,你可能真的发现JIT编译器完全优化了方法体......)< / p>

另一种可能的解释是,原始的Java代码开始时是非常悲观的,他们认为优化它不会产生重大影响。考虑一位经验丰富的Java程序员将其编写为:

public static void main(String[] args) {
    StringBuilder sb = new StringBuilder();
    for (int i=0 ; i<10000 ; i++) {
        sb.append("really ");
    }
    sb.append("long string.");
    String s = sb.toString();
}

这将大致 4个数量级


UPDATE - 我使用链接的Q&amp; A中的代码链接来查找生成该代码的Java字节码编译器源中的实际位置:here

源代码中没有提示解释代码生成策略的“哑”性。


所以对你的一般性问题:

  

Javac的StringBuilder优化是否弊大于利?

没有

我的理解是编译器开发人员进行了广泛的基准测试,以确定(整体)StringBuilder优化是值得的。

你在一个编写得很糟糕的程序中找到了一个可以更好地优化的边缘情况(假设)。这还不足以得出优化“弊大于利”

答案 1 :(得分:1)

  

FWIW,我的基准测试(毫不奇怪)将手动concat()方法比在10,000个元素的循环中使用+ =快2到3倍。

我有兴趣看到您的基准,因为我的(基于优秀的JMH线束)显示+=略快于String.concat。当我们每循环迭代执行三次操作(s += "re"; s += "al"; s += "ly ";)时,+=几乎保持高效,而String.concat则显然会受到明显的因子影响。

我在Intel Xeon E5-2695 v2 @ 2.40GHz运行OpenJDK build 1.8.0_40-ea-b23上运行我的基准测试。有四种实现:

  • implicit,使用+=
  • explicit,为每个串联显式实例化StringBuilder,表示+= desugaring
  • concat,使用String.concat
  • smart,它使用一个StringBuilder,如Stephen C's answer

每个实现有两个版本:正常的一个,一个在循环体中执行三个操作。

这是数字。这是吞吐量,因此越高越好。误差是99.9%置信区间的界限。 (这是JMH的默认输出。)

Benchmark                      Mode  Cnt     Score     Error  Units
StringBuilderBench.smart      thrpt   30  5438.676 ± 352.088  ops/s
StringBuilderBench.implicit   thrpt   30    10.290 ±   0.878  ops/s
StringBuilderBench.concat     thrpt   30     9.685 ±   0.924  ops/s
StringBuilderBench.explicit   thrpt   30     9.078 ±   0.884  ops/s

StringBuilderBench.smart3     thrpt   30  3335.001 ± 115.600  ops/s
StringBuilderBench.implicit3  thrpt   30     9.303 ±   0.838  ops/s
StringBuilderBench.explicit3  thrpt   30     8.597 ±   0.237  ops/s
StringBuilderBench.concat3    thrpt   30     3.182 ±   0.228  ops/s

正如预期的那样,仅使用一个StringBuilder的智能实现比其他实现快得多。在其余的实现中,+=击败String.concat,它胜过显式的StringBuilder实例化。考虑到错误,他们都非常接近。

当每个循环执行三个操作时,所有实现都会受到一个小的(相对)命中,但String.concat除外,其吞吐量减少了3倍。

考虑到HotSpot对StringBuilder(和StringBuffer)进行了特定优化,这些结果并不令人惊讶 - 请参阅src/share/vm/opto/stringopts.cppcommit history for this file显示这些优化日期为2009年末,作为错误JDK-6892658的一部分。

在8u5和8u40的早期访问版本之间似乎没有任何变化我运行基准测试,所以这并不能解释为什么我们得到不同的结果。 (当然,编译器中其他地方的更改也可能会改变结果。)


这是我使用java -jar benchmarks.jar -w 5s -wi 10 -r 5s -i 30 -f 1运行的基准代码。基准测试运行的代码和完整日志为also available as a Gist

package com.jeffreybosboom.stringbuilderbench;

import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.State;

@State(Scope.Thread)
public class StringBuilderBench {
    //promote to non-final fields to inhibit constant folding (see JMHSample_10_ConstantFold.java)
    private String really = "really ", long_string = "long string.", re = "re", al = "al", ly = "ly ";
    @Benchmark
    public String implicit() {
        String s = "";
        for (int i = 0; i < 10000; i++)
            s += really;
        s += long_string;
        return s;
    }
    @Benchmark
    public String explicit() {
        String s = "";
        for (int i = 0; i < 10000; i++)
            s = new StringBuilder().append(s).append(really).toString();
        s = new StringBuilder().append(s).append(long_string).toString();
        return s;
    }
    @Benchmark
    public String concat() {
        String s = "";
        for (int i = 0; i < 10000; i++)
            s = s.concat(really);
        s = s.concat(long_string);
        return s;
    }
    @Benchmark
    public String implicit3() {
        String s = "";
        for (int i = 0; i < 10000; i++) {
            s += re;
            s += al;
            s += ly;
        }
        s += long_string;
        return s;
    }
    @Benchmark
    public String explicit3() {
        String s = "";
        for (int i = 0; i < 10000; i++) {
            s = new StringBuilder().append(s).append(re).toString();
            s = new StringBuilder().append(s).append(al).toString();
            s = new StringBuilder().append(s).append(ly).toString();
        }
        s = new StringBuilder().append(s).append(long_string).toString();
        return s;
    }
    @Benchmark
    public String concat3() {
        String s = "";
        for (int i = 0; i < 10000; i++) {
            s = s.concat(re);
            s = s.concat(al);
            s = s.concat(ly);
        }
        s = s.concat(long_string);
        return s;
    }
    @Benchmark
    public String smart() {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 10000; i++)
            sb.append(really);
        sb.append(long_string);
        return sb.toString();
    }
    @Benchmark
    public String smart3() {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 10000; i++) {
            sb.append(re);
            sb.append(al);
            sb.append(ly);
        }
        sb.append(long_string);
        return sb.toString();
    }
}