2016-06-07 127 views
2

我在Java中拥有以下代码片段。如何在不重用目标变量的情况下连续调用StringBuffer(或StringBuilder)上的append()方法来提高性能

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

+6

尝试使用'javap'反编译这两种样式以查看区别。虽然字节码存在差异,但这感觉就像JIT可能为您处理的不必要的微观优化。 –

+0

@AndyTurner的差异将被HotSpot优化;您在运行时不会看到任何性能差异 – yole

+5

总是如此担心这些新程序员的表现。 – Kayaman

回答

5

是不是真的有什么不同?

让我们从分析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不应该优化它吗?

有人可能会争辩说,一旦虚拟机升温,JRE应该能够优化这些指令。但是,这种声明需要支持,并且仍然只适用于长时间运行的流程。

因此,让我们来检查一下这个说法,即使在热身之后也要验证性能。让我们用江铃控股进行基准测试此行为:

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

它甚至有关系吗?

嗯,这取决于。这当然是一个微观优化。如果一个系统使用Bubble Sort,肯定会有比这更紧迫的性能问题。并非所有的程序都有相同的要求,因此并不都需要遵循相同的规则。

此PMD规则可能仅对重视性能的特定项目有意义,并且将尽一切努力来削减几个ms。这些项目通常会使用几个不同的分析器,微型基准和其他工具。而像PMD这样的工具能够持续关注特定模式,肯定会对他们有所帮助。

PMD有许多其他规则可用,这可能适用于许多其他项目。仅仅因为这个特定的规则可能不适用于你的项目并不意味着这个工具没有用处,只需花时间审查可用的规则并选择那些对你真正重要的规则。

希望能够为每个人清除它。

相关问题