【问题标题】:Does Javac's StringBuilder optimisation do more harm than good?Javac StringBuilder 优化是否弊大于利?
【发布时间】:2014-08-28 04:21:34
【问题描述】:

假设我们有如下代码:

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() 方法比在循环中使用 += 快 2x3 倍10,000 个元素。当然,使用手动StringBuilder 总是更好的方法,但编译器肯定也不应该对+= 方法的性能产生不利影响吗?

【问题讨论】:

  • 你能链接到它被称为优化的地方吗?
  • @SotiriosDelimanolis 当然:docs.oracle.com/javase/specs/jls/se5.0/html/…
  • 我在Java 6上试了一下(第一次用javap,万岁),输出完全一样。
  • 这显然不是重复的 Jarrod!至于实际问题,我想没有人会费心尝试编写一些东西来将循环中的字符串连接转换为等效的字符串构建器设置 - 我想不出任何可以阻止它的东西,但我认为我缺少一些东西这意味着如果不是这种情况,您将无法自动化它。
  • 如果不是因为在字符串本质上是可变的语言中它不会是愚蠢的,我会同意。实际上,想法的表达对我来说很重要,您希望对想法的表示进行一些巧妙的解释。字符串连接非常普遍,并且与不变性的概念完全不兼容(因为你总是会得到一些新的东西,但会带来相关的性能成本),所以我们有这种折衷方案,这显然是令人困惑的。如果将 += 运算符扩展到 StringBuilders 以进行追加,则可能一切正常 - 看起来都一样。

标签: java string optimization javac 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 字节码编译器源中的实际位置:here

源代码中没有任何提示可以解释代码生成策略的“愚蠢”。


所以对于你的一般问题:

Javac 的 StringBuilder 优化弊大于利吗?

没有。

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

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

【讨论】:

  • 嗯,有点同意,但你在这里所说的确实与 Java 开发的“编写愚蠢的代码”不成文的规则相矛盾。 oracle.com/technetwork/articles/javase/devinsight-1-139780.html 。为什么编译器不能对此进行优化并迎合经验不足的 Java 开发人员?
  • @Gimby - 1) 是的。这是该规则的一个众所周知的例外。 2) 询问 Java 编译器团队。我能给你的只是可能的解释。
  • @Gimby - 因为编译器编写者不可能为“哑”的所有可能变体处理“哑代码”。
  • @jtahlborn - 因为他们必须在某处划清界限。
  • 完全:我猜够公平的;)
【解决方案2】:

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

我有兴趣查看您的基准,因为我的(基于出色的 JMH 工具)表明 +=String.concat 稍快。当我们在每次循环迭代 (s += "re"; s += "al"; s += "ly ";) 执行三个操作时,+= 的性能几乎保持不变,而 String.concat 则受到了明显的 3 倍影响。

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

  • 隐式,使用+=
  • explicit,为每个连接显式实例化一个StringBuilder,代表+=去糖
  • 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();
    }
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2017-07-18
    • 1970-01-01
    • 1970-01-01
    • 2014-11-18
    • 2012-10-04
    • 2011-06-27
    • 2011-07-06
    • 2011-08-02
    相关资源
    最近更新 更多