[java] 루프에서 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;
    }
}

견고한 코드를 작성하는 철학에서 메서드의 내부 동작은 메서드를 사용하는 객체로부터 숨겨져 야합니다. 따라서 루프 내에서 또는 루프 외부에서 StringBuilder를 재 선언하든 시스템의 관점에서 차이가 없습니다. 루프 외부에서 선언하는 것이 더 빠르며 코드를 읽기가 더 복잡하게 만들지 않으므로 다시 인스턴스화하기보다는 객체를 재사용하십시오.

코드가 더 복잡하고 객체 인스턴스화가 병목이라는 것을 확실히 알았더라도 주석을 달아주세요.

이 답변으로 세 번 실행 :

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

다른 답변으로 세 번 실행 :

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

중요하지는 않지만 StringBuilder의 초기 버퍼 크기를 설정하면 약간의 이득을 얻을 수 있습니다.


답변

좋아요, 이제 무슨 일이 일어나고 있는지 이해하고 말이 되네요.

나는 사본을 가져 가지 않은 String 생성자에 toString기본 char[]을 전달 했다는 인상을 받았습니다 . 그러면 다음 “쓰기”작업 (예 :)에서 복사본이 만들어집니다 . 나는이 믿고 있었다 의 경우 일부 이전 버전입니다. (지금은 아닙니다.) 그러나 아니요- 배열 (및 인덱스 및 길이)을 복사본 을받는 공용 생성자에 전달합니다.deleteStringBuffertoStringString

따라서 “재사용 StringBuilder“의 경우에는 버퍼에있는 동일한 문자 배열을 사용하여 문자열 당 하나의 데이터 복사본을 생성합니다. 분명히 StringBuilder매번 새로운 것을 생성하면 새로운 기본 버퍼가 생성되고 그 버퍼는 새로운 문자열을 생성 할 때 복사됩니다 (우리의 특정 경우에는 다소 의미가 없지만 안전상의 이유로 수행됨).

이 모든 것이 두 번째 버전이 확실히 더 효율적으로 이어지지 만 동시에 나는 여전히 더 나쁜 코드라고 말할 것입니다.


답변

String 연결을 볼 때 자동으로 StringBuilders (StringBuffers pre-J2SE 5.0)를 생성하는 Sun Java 컴파일러에 내장 된 최적화로 인해 아직 지적되지 않았다고 생각하므로 질문의 첫 번째 예는 다음과 같습니다.

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

더 읽기 쉬운 IMO, 더 나은 접근 방식입니다. 최적화하려는 시도는 일부 플랫폼에서 이익을 얻을 수 있지만 잠재적으로 다른 플랫폼에서 손실을 입을 수 있습니다.

그러나 실제로 성능 문제가 발생하면 최적화하십시오. Jon Skeet에 따라 StringBuilder의 버퍼 크기를 명시 적으로 지정하는 것으로 시작합니다.


답변

현대 JVM은 이와 같은 것에 대해 정말 똑똑합니다. 나는 두 번째로 그것을 추측하지 않고 유지 보수 및 가독성이 떨어지는 해킹을하지 않을 것입니다 … 사소하지 않은 성능 향상을 검증하는 프로덕션 데이터로 적절한 벤치 마크를 수행하지 않는 한 (그리고 문서화합니다.)


답변

Windows에서 소프트웨어를 개발 한 경험을 바탕으로 루프 중에 StringBuilder를 지우는 것이 각 반복마다 StringBuilder를 인스턴스화하는 것보다 성능이 더 좋다고 말합니다. 이를 지우면 추가 할당없이 즉시 덮어 쓸 수 있도록 해당 메모리가 해제됩니다. Java 가비지 수집기에 익숙하지 않지만 (다음 문자열이 StringBuilder를 늘리지 않는 한) 해제하고 재할 당하지 않는 것이 인스턴스화보다 더 유익하다고 생각합니다.

(제 의견은 다른 사람들이 제안하는 것과 상반됩니다. 흠. 벤치마킹 할 시간입니다.)