代码之家  ›  专栏  ›  技术社区  ›  Pier Luigi

在循环中重用StringBuilder更好吗?

  •  95
  • Pier Luigi  · 技术社区  · 18 年前

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

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

    正在实例化 字符串拼接 在每个循环周期都是一个好的解决方案?而调用delete是否更好,如下面所示?

    StringBuilder sb = new StringBuilder();
    for (loop condition) {
        sb.delete(0, sb.length);
        sb.append("some string");
        . . .
        sb.append(anotherString);
        . . .
        passToMethod(sb.toString());
    }
    
    14 回复  |  直到 12 年前
        1
  •  70
  •   Epaga Alex Reynolds    11 年前

    在我的迷你基准测试中,第二个大约快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
    
        2
  •  6
  •   Stu Thompson Helter Scelter    18 年前

    现代的JVM在这方面非常聪明。我不会猜测它,做一些不太容易维护/可读的黑客行为。。。除非你用生产数据做了适当的基准点,以验证一个非平凡的性能改进(并记录它;)

        3
  •  26
  •   Dave Jarvis James Eichele    6 年前

    更快:

    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
    

    虽然不重要,但设置 字符串拼接 的初始缓冲区大小,以防止内存重新分配,将提供一个小的性能增益。

        4
  •  25
  •   Jon Skeet    18 年前

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

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

    for (loop condition) {
      StringBuilder sb = new StringBuilder(4096);
    }
    
        5
  •  12
  •   Dave Jarvis James Eichele    11 年前

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

    我的印象是 toString 刚刚通过了考试 char[] 转换为字符串构造函数 没有 复印一份。然后在下一次“写入”操作(例如。 delete ).我相信 是 关于 StringBuffer 在以前的版本中。(现在不是。)但是没有- 托斯特林 只需将数组(以及索引和长度)传递给公众 String 获取副本的构造函数。

    所以在“重用 StringBuilder “如果我们真的为每个字符串创建一个数据副本,在整个缓冲区中使用相同的字符数组。显然,我们创建了一个新的 字符串拼接 每次都会创建一个新的底层缓冲区,然后在创建新字符串时复制该缓冲区(在我们的特定情况下,这是毫无意义的,但出于安全原因)。

    所有这些都使得第二个版本的效率更高,但同时我仍然认为它的代码更丑陋。

        6
  •  9
  •   Jack Leow    18 年前

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

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

    在国际海事组织,这是更好的方法。你的优化尝试可能会在某些平台上带来收益,但可能会在其他平台上造成损失。

    但如果你真的遇到了性能问题,那么当然,要不断优化。不过,根据Jon Skeet的说法,我首先要明确指定StringBuilder的缓冲区大小。

        7
  •  4
  •   cfeduke    18 年前

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

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

        8
  •  1
  •   brianegge    17 年前

    执行“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%左右。

        9
  •  1
  •   Ting Choo Chiaw    15 年前

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

    在频繁检索不需要频繁更改的字符串值的情况下,Stringbuilder允许更高的字符串检索性能。这就是使用Stringbuilder的目的。。请不要错误地测试它的核心目的。。

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

        10
  •  1
  •   johnmartel    13 年前

    速度并没有显著提高,但从我的测试来看,使用1.6.0_45 64位平均速度要快几毫秒: 使用StringBuilder。setLength(0)而不是StringBuilder。删除():

    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 );
    
        11
  •  1
  •   Shen liang    13 年前

    最快的方法是使用“设置长度”。它不涉及复制操作。 创建新StringBuilder的方法应该完全过时 .对于StringBuilder来说,速度很慢。删除(int start,int end)是因为它会为调整大小的部分再次复制数组。

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

    之后,StringBuilder。delete()将更新StringBuilder。数到新的尺寸。而StringBuilder。setLength()只需简化更新 StringBuilder。计数 换成新尺寸。

        12
  •  0
  •   dongilmore    18 年前

    第一种更适合人类。如果在某些JVM的某些版本上,第二个速度稍微快一点,那又怎样?

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

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

        13
  •  0
  •   Ulrich K.    7 年前

    我认为这样优化性能是没有意义的。 今天(2019年),在我的I5笔记本电脑上,这两个状态运行了大约11秒,循环次数为10万次:

        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
  •   Peter Mortensen Pieter Jan Bonestroo    16 年前

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

    推荐文章