如果在StringBuffer(或StringBuilder)上连续调用append()而不重用目标变量,我该如何提高性能

时间:2016-06-07 07:14:27

标签: java stringbuilder pmd stringbuffer

我在Java中有以下代码。

String foo = " ";

方法1:

StringBuffer buf = new StringBuffer();
buf.append("Hello");
buf.append(foo);
buf.append("World");  

方法2:

StringBuffer buf = new StringBuffer();
buf.append("Hello").append(foo).append("World");

有人可以启发我,方法2如何提高代码的性能?

https://pmd.github.io/pmd-5.4.2/pmd-java/rules/java/strings.html#ConsecutiveAppendsShouldReuse

1 个答案:

答案 0 :(得分:7)

它真的不一样吗?

让我们从分析javac输出开始。鉴于代码:

public class Main {
  public String appendInline() {
    final StringBuilder sb = new StringBuilder().append("some").append(' ').append("string");
    return sb.toString();
  }

  public String appendPerLine() {
    final StringBuilder sb = new StringBuilder();
    sb.append("some");
    sb.append(' ');
    sb.append("string");
    return sb.toString();
  }
}

我们使用javac进行编译,并使用javap -c -s

检查输出
  public java.lang.String appendInline();
    descriptor: ()Ljava/lang/String;
    Code:
       0: new           #2                  // class java/lang/StringBuilder
       3: dup
       4: invokespecial #3                  // Method java/lang/StringBuilder."<init>":()V
       7: ldc           #4                  // String some
       9: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      12: bipush        32
      14: invokevirtual #6                  // Method java/lang/StringBuilder.append:(C)Ljava/lang/StringBuilder;
      17: ldc           #7                  // String string
      19: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      22: astore_1
      23: aload_1
      24: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      27: areturn

  public java.lang.String appendPerLine();
    descriptor: ()Ljava/lang/String;
    Code:
       0: new           #2                  // class java/lang/StringBuilder
       3: dup
       4: invokespecial #3                  // Method java/lang/StringBuilder."<init>":()V
       7: astore_1
       8: aload_1
       9: ldc           #4                  // String some
      11: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      14: pop
      15: aload_1
      16: bipush        32
      18: invokevirtual #6                  // Method java/lang/StringBuilder.append:(C)Ljava/lang/StringBuilder;
      21: pop
      22: aload_1
      23: ldc           #7                  // String string
      25: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      28: pop
      29: aload_1
      30: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      33: areturn

如图所示,appendPerLine变量产生了一个更大的字节码,通过生成几个额外的aload_1pop指令基本上相互抵消(将字符串构建器/缓冲区留在其中)堆叠,并将其删除以丢弃它)。反过来,这意味着JRE将产生更大的呼叫站点并且具有更大的开销。相反,较小的调用点可以提高JVM内联方法调用的几率,减少方法调用开销并进一步提高性能。

仅在链接方法调用时,这可以从冷启动中提高性能。

JVM不应该优化它吗?

有人可能会争辩说,一旦VM预热,JRE应该能够优化这些指令。但是,此声明需要支持,并且仍然只适用于长时间运行的进程。

因此,让我们检查一下这个说法,并在热身后验证效果。让我们使用JMH来衡量这种行为:

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

@State(Scope.Benchmark)
public class StringBenchmark {
    private String from = "Alex";
    private String to = "Readers";
    private String subject = "Benchmarking with JMH";

    @Param({"16"})
    private int size;

    @Benchmark
    public String testEmailBuilderSimple() {
        StringBuilder builder = new StringBuilder(size);
        builder.append("From");
        builder.append(from);
        builder.append("To");
        builder.append(to);
        builder.append("Subject");
        builder.append(subject);
        return builder.toString();
    }

    @Benchmark
    public String testEmailBufferSimple() {
        StringBuffer buffer = new StringBuffer(size);
        buffer.append("From");
        buffer.append(from);
        buffer.append("To");
        buffer.append(to);
        buffer.append("Subject");
        buffer.append(subject);
        return buffer.toString();
    }

    @Benchmark
    public String testEmailBuilderChain() {
        return new StringBuilder(size).append("From").append(from).append("To").append(to).append("Subject")
                .append(subject).toString();
    }

    @Benchmark
    public String testEmailBufferChain() {
        return new StringBuffer(size).append("From").append(from).append("To").append(to).append("Subject")
                .append(subject).toString();
    }
}

我们编译并运行它,我们获得:

Benchmark                               (size)   Mode  Cnt         Score        Error  Units
StringBenchmark.testEmailBufferChain        16  thrpt  200  22981842.957 ± 238502.907  ops/s
StringBenchmark.testEmailBufferSimple       16  thrpt  200   5789967.103 ±  62743.660  ops/s
StringBenchmark.testEmailBuilderChain       16  thrpt  200  22984472.260 ± 212243.175  ops/s
StringBenchmark.testEmailBuilderSimple      16  thrpt  200   5778824.788 ±  59200.312  ops/s

因此,即使在预热之后,遵循该规则也会使吞吐量提高约4倍。所有这些运行都是使用Oracle JRE 8u121完成的。

当然,你不必相信我,others have done similar analysis你甚至可以try it yourself

甚至重要吗?

嗯,这取决于。这当然是一种微观优化。如果系统使用冒泡排序,那么肯定存在比此更紧迫的性能问题。并非所有程序都有相同的要求,因此并非都需要遵循相同的规则。

这个PMD规则可能只对那些重视性能的特定项目有意义,并且可以做任何削减几毫秒的事情。这些项目通常使用几种不同的分析器,微基准测试和其他工具。让PMD等工具密切关注特定模式肯定有助于他们。

PMD还有许多其他规则,可能适用于许多其他项目。仅仅因为此特定规则可能不适用于您的项目并不意味着该工具无用,只需花时间查看可用规则并选择对您真正重要的规则。

希望能为所有人清除它。