在循环中重用StringBuilder会更好吗?

时间:2008-10-28 07:09:38

标签: java performance string stringbuilder

我有一个关于使用StringBuilder的性能相关问题。 在一个很长的循环中,我正在操纵StringBuilder并将其传递给另一个方法:

for (loop condition) {
    StringBuilder sb = new StringBuilder();
    sb.append("some string");
    . . .
    sb.append(anotherString);
    . . .
    passToMethod(sb.toString());
}

在每个循环周期实例化StringBuilder是一个很好的解决方案吗?并且更好地调用删除,如下所示?

StringBuilder sb = new StringBuilder();
for (loop condition) {
    sb.delete(0, sb.length);
    sb.append("some string");
    . . .
    sb.append(anotherString);
    . . .
    passToMethod(sb.toString());
}

15 个答案:

答案 0 :(得分:66)

第二个在我的迷你基准测试中快了大约25%。

public class ScratchPad {

    static String a;

    public static void main( String[] args ) throws Exception {
        long time = System.currentTimeMillis();
        for( int i = 0; i < 10000000; i++ ) {
            StringBuilder sb = new StringBuilder();
            sb.append( "someString" );
            sb.append( "someString2"+i );
            sb.append( "someStrin4g"+i );
            sb.append( "someStr5ing"+i );
            sb.append( "someSt7ring"+i );
            a = sb.toString();
        }
        System.out.println( System.currentTimeMillis()-time );
        time = System.currentTimeMillis();
        StringBuilder sb = new StringBuilder();
        for( int i = 0; i < 10000000; i++ ) {
            sb.delete( 0, sb.length() );
            sb.append( "someString" );
            sb.append( "someString2"+i );
            sb.append( "someStrin4g"+i );
            sb.append( "someStr5ing"+i );
            sb.append( "someSt7ring"+i );
            a = sb.toString();
        }
        System.out.println( System.currentTimeMillis()-time );
    }
}

结果:

25265
17969

请注意,这是使用JRE 1.6.0_07。


基于Jon Skeet在编辑中的想法,这里是版本2.但是结果相同。

public class ScratchPad {

    static String a;

    public static void main( String[] args ) throws Exception {
        long time = System.currentTimeMillis();
        StringBuilder sb = new StringBuilder();
        for( int i = 0; i < 10000000; i++ ) {
            sb.delete( 0, sb.length() );
            sb.append( "someString" );
            sb.append( "someString2" );
            sb.append( "someStrin4g" );
            sb.append( "someStr5ing" );
            sb.append( "someSt7ring" );
            a = sb.toString();
        }
        System.out.println( System.currentTimeMillis()-time );
        time = System.currentTimeMillis();
        for( int i = 0; i < 10000000; i++ ) {
            StringBuilder sb2 = new StringBuilder();
            sb2.append( "someString" );
            sb2.append( "someString2" );
            sb2.append( "someStrin4g" );
            sb2.append( "someStr5ing" );
            sb2.append( "someSt7ring" );
            a = sb2.toString();
        }
        System.out.println( System.currentTimeMillis()-time );
    }
}

结果:

5016
7516

答案 1 :(得分:25)

在编写可靠代码的哲学中,将StringBuilder放在循环中总是更好。这样它就不会超出其预期的代码。

其次,StringBuilder中最大的改进来自给它一个初始大小,以避免它在循环运行时变大;

for (loop condition) {
  StringBuilder sb = new StringBuilder(4096);
}

答案 2 :(得分:23)

更快:

public class ScratchPad {

    private static String a;

    public static void main( String[] args ) throws Exception {
        long time = System.currentTimeMillis();
        StringBuilder sb = new StringBuilder( 128 );

        for( int i = 0; i < 10000000; i++ ) {
            // Resetting the string is faster than creating a new object.
            // Since this is a critical loop, every instruction counts.
            //
            sb.setLength( 0 );
            sb.append( "someString" );
            sb.append( "someString2" );
            sb.append( "someStrin4g" );
            sb.append( "someStr5ing" );
            sb.append( "someSt7ring" );
            setA( sb.toString() );
        }

        System.out.println( System.currentTimeMillis()-time );
    }

    private static void setA( String aString ) {
        a = aString;
    }
}

在编写固体代码的哲学中,应该从使用该方法的对象中隐藏该方法的内部工作方式。因此,无论您是在循环内还是循环外重新声明StringBuilder,系统的视角都没有区别。由于在循环之外声明它更快,并且它不会使代码更复杂,因此重用该对象而不是重新实现它。

即使代码更复杂,并且您确实知道对象实例化是瓶颈,请对其进行评论。

有三个回答:

$ java ScratchPad
1567
$ java ScratchPad
1569
$ java ScratchPad
1570

另外三个回答:

$ java ScratchPad2
1663
2231
$ java ScratchPad2
1656
2233
$ java ScratchPad2
1658
2242

虽然不重要,但设置StringBuilder的初始缓冲区大小会产生很小的收益。

答案 3 :(得分:12)

好的,我现在明白发生了什么,而且确实有意义。

我的印象是toString刚刚将基础char[]传递给了一个没有获取副本的String构造函数。然后将在下一次“写入”操作(例如delete)上进行复制。我相信在以前的某个版本中, 就是StringBuffer的情况。 (现在不是。)但是没有 - toString只是将数组(以及索引和长度)传递给需要副本的公共String构造函数。

因此,在“重用StringBuilder”的情况下,我们真正为每个字符串创建一个数据副本,一直使用缓冲区中的相同char数组。显然每次都创建一个新的StringBuilder创建一个新的底层缓冲区 - 然后在创建一个新字符串时复制该缓冲区(在我们的特定情况下,有些无意义,但出于安全原因)。

这一切导致第二个版本肯定更有效率 - 但与此同时我仍然会说这是更丑陋的代码。

答案 4 :(得分:9)

由于我不认为它已被指出,因为Sun Java编译器内置了优化,当它看到String连接时自动创建StringBuilders(StringBuffers pre-J2SE 5.0),问题中的第一个示例是等效的到:

for (loop condition) {
  String s = "some string";
  . . .
  s += anotherString;
  . . .
  passToMethod(s);
}

哪个更具可读性,IMO,更好的方法。您尝试优化可能会导致某些平台获益,但可能会损失其他平台。

但如果你真的遇到了性能问题,那么请确保优化。我首先明确指定StringBuilder的缓冲区大小,每个Jon Skeet。

答案 5 :(得分:4)

现代JVM对这样的东西非常聪明。我不会再猜测它并做一些不太可维护/可读的hacky ......除非你用生产数据做适当的基准测试,以验证非平凡的性能改进(并记录它;)

答案 6 :(得分:4)

根据我在Windows上开发软件的经验,我会说在循环期间清除StringBuilder比每次迭代实例化StringBuilder有更好的性能。清除它可以立即释放内存,无需额外分配。我对Java垃圾收集器不够熟悉,但我认为释放和不重新分配(除非你的下一个字符串增长了StringBuilder)比实例化更有益。

(我的意见与其他人的建议相反。嗯。是时候对它进行基准测试了。)

答案 7 :(得分:1)

执行'setLength'或'delete'改善性能的原因主要是代码'学习'正确的缓冲区大小,而不是进行内存分配。通常,I recommend letting the compiler do the string optimizations。但是,如果性能很关键,我会经常预先计算缓冲区的预期大小。默认的StringBuilder大小为16个字符。如果你超越了那个,那么它必须调整大小。调整大小是性能丢失的地方。这是另一个迷你基准,说明了这一点:

private void clear() throws Exception {
    long time = System.currentTimeMillis();
    int maxLength = 0;
    StringBuilder sb = new StringBuilder();

    for( int i = 0; i < 10000000; i++ ) {
        // Resetting the string is faster than creating a new object.
        // Since this is a critical loop, every instruction counts.
        //
        sb.setLength( 0 );
        sb.append( "someString" );
        sb.append( "someString2" ).append( i );
        sb.append( "someStrin4g" ).append( i );
        sb.append( "someStr5ing" ).append( i );
        sb.append( "someSt7ring" ).append( i );
        maxLength = Math.max(maxLength, sb.toString().length());
    }

    System.out.println(maxLength);
    System.out.println("Clear buffer: " + (System.currentTimeMillis()-time) );
}

private void preAllocate() throws Exception {
    long time = System.currentTimeMillis();
    int maxLength = 0;

    for( int i = 0; i < 10000000; i++ ) {
        StringBuilder sb = new StringBuilder(82);
        sb.append( "someString" );
        sb.append( "someString2" ).append( i );
        sb.append( "someStrin4g" ).append( i );
        sb.append( "someStr5ing" ).append( i );
        sb.append( "someSt7ring" ).append( i );
        maxLength = Math.max(maxLength, sb.toString().length());
    }

    System.out.println(maxLength);
    System.out.println("Pre allocate: " + (System.currentTimeMillis()-time) );
}

public void testBoth() throws Exception {
    for(int i = 0; i < 5; i++) {
        clear();
        preAllocate();
    }
}

结果显示重用该对象比创建预期大小的缓冲区快10%。

答案 8 :(得分:1)

LOL,我第一次看到人们通过在StringBuilder中组合字符串来比较性能。为此,如果你使用“+”,它可能会更快; D。使用StringBuilder加速检索整个字符串作为“locality”的概念的目的。

在频繁检索不需要频繁更改的String值的场景中,Stringbuilder允许更高的字符串检索性能。这就是使用Stringbuilder的目的..请不要MIS-测试那个的核心目的..

有人说,飞机飞得更快。因此,我用我的自行车测试它,发现飞机移动得更慢。你知道我如何设置实验设置; D

答案 9 :(得分:1)

没有明显更快,但从我的测试中,它使用1.6.0_45 64位平均显示速度快几倍: 使用StringBuilder.setLength(0)而不是StringBuilder.delete():

time = System.currentTimeMillis();
StringBuilder sb2 = new StringBuilder();
for (int i = 0; i < 10000000; i++) {
    sb2.append( "someString" );
    sb2.append( "someString2"+i );
    sb2.append( "someStrin4g"+i );
    sb2.append( "someStr5ing"+i );
    sb2.append( "someSt7ring"+i );
    a = sb2.toString();
    sb2.setLength(0);
}
System.out.println( System.currentTimeMillis()-time );

答案 10 :(得分:1)

最快的方法是使用“setLength”。它不涉及复制操作。 创建一个新的StringBuilder的方法应该是完全。 StringBuilder.delete(int start,int end)的速度很慢,因为它会为调整大小部分再次复制数组。

 System.arraycopy(value, start+len, value, start, count-end);

之后,StringBuilder.delete()会将StringBuilder.count更新为新大小。而StringBuilder.setLength()只是简化了将 StringBuilder.count 更新为新的大小。

答案 11 :(得分:0)

唉,我正常使用C#,我认为清除StringBuilder的重量超过了实现新的权重。

答案 12 :(得分:0)

第一种对人类更好。如果在某些JVM的某些版本上第二个更快一点,那么呢?

如果性能非常重要,请绕过StringBuilder并编写自己的。如果您是一名优秀的程序员,并考虑到您的应用程序如何使用此功能,您应该能够更快地完成它。值得吗?可能不是。

为什么这个问题被视为“最喜欢的问题”?因为性能优化非常有趣,无论它是否实用。

答案 13 :(得分:0)

我认为尝试这样优化性能没有意义。 今天(2019年),这两个陈述在我的I5笔记本电脑上以100.000.000循环运行了大约11秒:

    String a;
    StringBuilder sb = new StringBuilder();
    long time = 0;

    System.gc();
    time = System.currentTimeMillis();
    for (int i = 0; i < 100000000; i++) {
        StringBuilder sb3 = new StringBuilder();
        sb3.append("someString");
        sb3.append("someString2");
        sb3.append("someStrin4g");
        sb3.append("someStr5ing");
        sb3.append("someSt7ring");
        a = sb3.toString();
    }
    System.out.println(System.currentTimeMillis() - time);

    System.gc();
    time = System.currentTimeMillis();
    for (int i = 0; i < 100000000; i++) {
        sb.setLength(0);
        sb.delete(0, sb.length());
        sb.append("someString");
        sb.append("someString2");
        sb.append("someStrin4g");
        sb.append("someStr5ing");
        sb.append("someSt7ring");
        a = sb.toString();
    }
    System.out.println(System.currentTimeMillis() - time);

==> 11000毫秒(循环内声明)和8236毫秒(循环外声明)

即使我正在运行带有数十亿循环的地址去重复的程序 相差2秒。 1亿个循环没有任何区别,因为程序运行了几个小时。 另外请注意,如果只有一个append语句,情况会有所不同:

    System.gc();
    time = System.currentTimeMillis();
    for (int i = 0; i < 100000000; i++) {
        StringBuilder sb3 = new StringBuilder();
        sb3.append("someString");
            a = sb3.toString();
    }
    System.out.println(System.currentTimeMillis() - time);

    System.gc();
    time = System.currentTimeMillis();
    for (int i = 0; i < 100000000; i++) {
        sb.setLength(0);
        sb.delete(0, sb.length());
        sb.append("someString");
        a = sb.toString();
    }
    System.out.println(System.currentTimeMillis() - time);

==> 3416毫秒(内部循环),3555毫秒(外部循环) 在这种情况下,在循环内创建StringBuilder的第一条语句更快。 而且,如果您更改执行顺序,则速度会更快:

    System.gc();
    time = System.currentTimeMillis();
    for (int i = 0; i < 100000000; i++) {
        sb.setLength(0);
        sb.delete(0, sb.length());
        sb.append("someString");
        a = sb.toString();
    }
    System.out.println(System.currentTimeMillis() - time);

    System.gc();
    time = System.currentTimeMillis();
    for (int i = 0; i < 100000000; i++) {
        StringBuilder sb3 = new StringBuilder();
        sb3.append("someString");
            a = sb3.toString();
    }
    System.out.println(System.currentTimeMillis() - time);

==> 3638毫秒(外部循环),2908毫秒(内部循环)

关于, 乌尔里希

答案 14 :(得分:-2)

声明一次,并分配每次。与优化相比,它是一个更实用,更可重用的概念。