Programing

루프에서 StringBuilder를 재사용하는 것이 더 낫습니까?

crosscheck 2020. 8. 24. 07:35
반응형

루프에서 StringBuilder를 재사용하는 것이 더 낫습니까?


StringBuilder 사용에 관한 성능 관련 질문이 있습니다. 매우 긴 루프에서 a를 조작하고 다음 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());
}

두 번째는 내 미니 벤치 마크에서 약 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

견고한 코드를 작성하는 철학에서는 StringBuilder를 루프에 넣는 것이 항상 좋습니다. 이렇게하면 의도 한 코드를 벗어나지 않습니다.

둘째, StringBuilder의 가장 큰 개선은 루프가 실행되는 동안 더 커지지 않도록 초기 크기를 제공하는 것입니다.

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

더 빠른 속도 :

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;
    }
}

In the philosophy of writing solid code, the inner workings of the method should be hidden from the objects that use the method. Thus it makes no difference from the system's perspective whether you redeclare the StringBuilder within the loop or outside of the loop. Since declaring it outside of the loop is faster, and it does not make the code more complicated to read, then reuse the object rather than reinstantiate it.

Even if the code was more complicated, and you knew for certain that object instantiation was the bottleneck, comment it.

Three runs with this answer:

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

Three runs with the other answer:

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

Although not significant, setting the StringBuilder's initial buffer size will give a small gain.


Okay, I now understand what's going on, and it does make sense.

I was under the impression that toString just passed the underlying char[] into a String constructor which didn't take a copy. A copy would then be made on the next "write" operation (e.g. delete). I believe this was the case with StringBuffer in some previous version. (It isn't now.) But no - toString just passes the array (and index and length) to the public String constructor which takes a copy.

So in the "reuse the StringBuilder" case we genuinely create one copy of the data per string, using the same char array in the buffer the whole time. Obviously creating a new StringBuilder each time creates a new underlying buffer - and then that buffer is copied (somewhat pointlessly, in our particular case, but done for safety reasons) when creating a new string.

All this leads to the second version definitely being more efficient - but at the same time I'd still say it's uglier code.


Since I don't think it's been pointed out yet, because of optimizations built into the Sun Java compiler, which automatically creates StringBuilders (StringBuffers pre-J2SE 5.0) when it sees String concatenations, the first example in the question is equivalent to:

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

Which is more readable, IMO, the better approach. Your attempts to optimize may result in gains in some platform, but potentially losses others.

But if you really are running into issues with performance, then sure, optimize away. I'd start with explicitly specifying the buffer size of the StringBuilder though, per Jon Skeet.


The modern JVM is really smart about stuff like this. I would not second guess it and do something hacky that is less maintainable/readable...unless you do proper bench marks with production data that validate a non-trivial performance improvement (and document it ;)


Based on my experience with developing software on Windows I would say clearing the StringBuilder out during your loop has better performance than instantiating a StringBuilder with each iteration. Clearing it frees that memory to be overwritten immediately with no additional allocation required. I'm not familiar enough with the Java garbage collector, but I would think that freeing and no reallocation (unless your next string grows the StringBuilder) is more beneficial than instantiation.

(My opinion is contrary to what everyone else is suggesting. Hmm. Time to benchmark it.)


The reason why doing a 'setLength' or 'delete' improves the performance is mostly the code 'learning' the right size of the buffer, and less to do the memory allocation. Generally, I recommend letting the compiler do the string optimizations. However, if the performance is critical, I'll often pre-calculate the expected size of the buffer. The default StringBuilder size is 16 characters. If you grow beyond that, then it has to resize. Resizing is where the performance is getting lost. Here's another mini-benchmark which illustrates this:

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

The results show reusing the object is about 10% faster than creating a buffer of the expected size.


LOL, first time i ever seen people compared the performance by combining string in StringBuilder. For that purpose, if you use "+", it could be even faster ;D. The purpose of using StringBuilder to speed up for retrieval of the whole string as the concept of "locality".

In the scenario that you retrieve a String value frequently that does not need frequent change, Stringbuilder allows higher performance of string retrieval. And that is the purpose of using Stringbuilder.. please do not MIS-Test the core purpose of that..

Some people said, Plane flies faster. Therefore, i test it with my bike, and found that the plane move slower. Do you know how i set the experiment settings ;D


Not significantly faster, but from my tests it shows on average to be a couple millis faster using 1.6.0_45 64 bits: use StringBuilder.setLength(0) instead of 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 );

The fastest way is to use "setLength". It won't involve the copying operation. The way to create a new StringBuilder should be completely out. The slow for the StringBuilder.delete(int start, int end) is because it will copy the array again for the resizing part.

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

After that, the StringBuilder.delete() will update the StringBuilder.count to the new size. While the StringBuilder.setLength() just simplify update the StringBuilder.count to the new size.


Alas I'm on C# normally, I think that clearing a StringBuilder weights more than instanciating a new one.


The first is better for humans. If the second is a bit faster on some versions of some JVMs, so what?

If performance is that critical, bypass StringBuilder and write your own. If you're a good programmer, and take into account how your app is using this function, you should be able to make it even faster. Worthwhile? Probably not.

Why is this question stared as "favorite question"? Because performance optimization is so much fun, no matter whether it is practical or not.


Declare once, and assign each time. It is a more pragmatic and reusable concept than an optimization.

참고URL : https://stackoverflow.com/questions/242438/is-it-better-to-reuse-a-stringbuilder-in-a-loop

반응형