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

标签 java stringbuilder pmd stringbuffer

我在 Java 中有以下一段代码。

String foo = " ";

方法一:
StringBuffer buf = new StringBuffer();
buf.append("Hello");
buf.append(foo);
buf.append("World");  

方法二:
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

最佳答案

真的不一样吗?

让我们从分析 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 有许多其他可用的规则,这些规则可能适用于许多其他项目。仅仅因为此特定规则可能不适用于您的项目并不意味着该工具没有用,只需花时间查看可用规则并选择对您真正重要的规则。

希望能为大家解惑。

关于java - 如果在 StringBuffer(或 StringBuilder)上连续调用 append() 而不重用目标变量,我如何提高性能,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/37672785/

相关文章:

java - 重命名的类中的重复字段(ASM + Jar)

java - 获取图像内部存储的文件路径

java - 模拟方法没有被调用 - java

Java CSV 格式问题

java - 在 Eclipse PMD 插件中,我可以引用标准规则集文件吗?

java - 为什么使用此 Integer.parseInt(x, y) 会出现 NumberFormatException?

c# - .NET StringBuilder 和逐字字符串文字

c# - StringBuilder.ToString() 的复杂性是什么

c++ - 抑制来自 CPD 的 C/C++ 代码警告

gradle - 忽略Gradle Pmd插件中的优先级3、4、5违规