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

当然,在编译时确定编译器生成的是否只追加一个值是微不足道的 - 如果是这种情况,请使用简单的concat操作来代替?StringBuilder

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


答案 1

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

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

一种可能的解释是,HotSpot JIT编译器中可能有代码将字节码序列优化为更好的内容。(如果您感到好奇,可以修改代码,以便对其进行JIT编译...,然后捕获并检查本机代码。但是,您实际上可能会发现JIT编译器完全优化了方法体...)

另一种可能的解释是,原始的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个数量级


更新 - 我使用链接的问答中的代码链接来查找生成该代码的Java字节码编译器源中的实际位置:这里

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


因此,对于您的一般问题:

Javac的StringBuilder优化弊大于利吗?

不。

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

你在一个写得不好的程序中发现了一个边缘情况,可以更好地优化(这是假设的)。这不足以得出优化总体上“弊大于利”的结论。


答案 2

FWIW,我的基准测试(不出所料)将手动concat()方法放在具有10,000个元素的循环中比使用+=快2x3倍。

我有兴趣看到您的基准测试,因为我的基准测试(基于出色的JMH线束)显示它比.当我们每次循环迭代()执行三次操作时,性能几乎一样好,同时采用明显的3因子命中。+=String.concats += "re"; s += "al"; s += "ly ";+=String.concat

我在英特尔至强E5-2695 v2 @ 2.40GHz上运行了我的基准测试,运行OpenJDK build 1.8.0_40-ea-b23。有四种实现:

  • 隐式,它使用+=
  • 显式,它为每个串联显式实例化 StringBuilder,表示脱糖+=
  • concat,它使用String.concat
  • smart,它使用一个StringBuilder,如Stephen C的答案

每个实现有两个版本:普通版本和一个在循环体中执行三个操作的版本。

以下是数字。这是吞吐量,所以越高越好。误差是 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的智能实现比其他StringBuilder快得多。在其余的实现中,beats ,它击败了显式 StringBuilder 实例化。考虑到错误,它们都相当接近。+=String.concat

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

考虑到HotSpot对StringBuilder(和StringBuffer)进行了特定的优化,这些结果并不奇怪 - 请参阅src/share/vm/opto/stringopts.cpp此文件的提交历史记录显示这些优化日期为 2009 年末,作为 bug JDK-6892658 的一部分。

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


下面是我使用的基准测试代码。基准测试运行的代码和完整日志也可用作要点java -jar benchmarks.jar -w 5s -wi 10 -r 5s -i 30 -f 1

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