104

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

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());
}
4

14 回答 14

70

在我的迷你基准测试中,第二个大约快 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
于 2008-10-28T07:17:45.183 回答
26

更快:

public class ScratchPad {

    private static String a;

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

        // Pre-allocate enough space to store all appended strings.
        // StringBuilder, ultimately, uses an array of characters.
        final 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( final 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的初始缓冲区大小以防止内存重新分配,将带来小的性能提升。

于 2009-06-22T06:20:33.010 回答
25

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

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

for (loop condition) {
  StringBuilder sb = new StringBuilder(4096);
}
于 2008-10-28T07:14:30.150 回答
12

好的,我现在明白发生了什么,这确实有道理。

我的印象是toString只是将底层传递给了一个带副本char[]的 String 构造函数。然后将在下一个“写”操作(例如)中进行复制。我相信在以前的某些版本中就是这种情况。(现在不是。)但是没有 -只是将数组(以及索引和长度)传递给获取副本的公共构造函数。deleteStringBuffertoStringString

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

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

于 2008-10-28T08:18:59.460 回答
9

由于我认为还没有被指出,由于 Sun Java 编译器内置的优化,当它看到字符串连接时会自动创建 StringBuilders(StringBuffers pre-J2SE 5.0),问题中的第一个示例相当于:

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

哪个更具可读性,IMO,更好的方法。您的优化尝试可能会在某些平台上带来收益,但可能会在其他平台上造成损失。

但是,如果您确实遇到了性能问题,那么当然可以优化。不过,根据 Jon Skeet,我将从显式指定 StringBuilder 的缓冲区大小开始。

于 2008-10-30T13:07:51.450 回答
6

现代 JVM 对这类事情非常聪明。我不会再猜测它并做一些难以维护/可读性较差的骇人听闻的事情......除非你对生产数据进行适当的基准测试,以验证非平凡的性能改进(并记录它;)

于 2008-10-28T07:12:29.093 回答
4

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

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

于 2008-10-28T07:17:24.777 回答
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更新为新大小。

于 2013-09-29T07:03:22.653 回答
1

执行“setLength”或“delete”提高性能的原因主要是代码“学习”正确的缓冲区大小,而不是进行内存分配。通常,我建议让编译器进行字符串优化。但是,如果性能很关键,我通常会预先计算缓冲区的预期大小。默认 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%。

于 2009-07-16T01:22:08.923 回答
1

大声笑,我第一次看到人们通过在 StringBuilder 中组合字符串来比较性能。为此,如果您使用“+”,它可能会更快;D。使用 StringBuilder 的目的是为了加快对整个字符串的检索,作为“局部性”的概念。

在需要频繁检索String值而不需要频繁更改的场景下,Stringbuilder可以提供更高的字符串检索性能。这就是使用 Stringbuilder 的目的.. 请不要 MIS-Test 的核心目的..

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

于 2010-12-17T05:05:00.933 回答
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 );
于 2013-06-05T13:53:05.123 回答
0

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

如果性能如此关键,请绕过 StringBuilder 并编写自己的。如果你是一个优秀的程序员,并且考虑到你的应用程序是如何使用这个函数的,你应该能够让它更快。值得吗?可能不是。

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

于 2008-10-30T00:32:23.457 回答
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 亿次循环没有任何区别,因为程序运行了几个小时。另请注意,如果您只有一个附加语句,情况会有所不同:

    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 msec(inside loop), 3555 msec(outside loop) 在这种情况下,在循环内创建 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 毫秒(内循环)

问候,乌尔里希

于 2019-10-22T11:41:30.150 回答
-2

声明一次,每次分配。这是一个比优化更实用和可重用的概念。

于 2008-10-28T13:52:05.153 回答