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


101

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

답변:


69

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

4
왜 이런 일이 발생할 수 있는지 설명하기 위해 내 대답에 편집을 추가했습니다. 잠시 (45 분) 더 자세히 살펴 볼게요. 추가 호출에서 연결을 수행하면 처음에 StringBuilder를 사용하는 포인트가 다소 줄어 듭니다. :)
Jon Skeet

3
또한 두 블록을 반대로하면 어떤 일이 발생하는지 보는 것도 흥미로울 것입니다. JIT는 첫 번째 테스트 동안 여전히 StringBuilder를 "워밍업"합니다. 관련이 없을 수도 있지만 시도해 보는 것은 흥미 롭습니다.
Jon Skeet

1
나는 그것이 더 깨끗 하기 때문에 여전히 첫 번째 버전을 사용할 것 입니다. 그러나 실제로 벤치 마크를 수행 한 것이 좋습니다. :) 다음 제안 변경 : 생성자에 전달 된 적절한 용량으로 # 1을 시도하십시오.
Jon Skeet

25
sb.setLength (0); 사용 대신 객체를 다시 만들거나 .delete ()를 사용하는 것에 대해 StringBuilder의 내용을 비우는 가장 빠른 방법입니다. 이것은 StringBuffer에는 적용되지 않으며 동시성 검사는 속도 이점을 무효화합니다.
P Arrayah

1
비효율적 인 대답. P Arrayah와 Dave Jarvis가 맞습니다. setLength (0)은 가장 효율적인 대답입니다. StringBuilder는 char 배열로 지원되며 변경 가능합니다. .toString ()이 호출되면 char 배열이 복사되고 변경 불가능한 문자열을 백업하는 데 사용됩니다. 이 시점에서 StringBuilder의 변경 가능한 버퍼는 삽입 포인터를 다시 0으로 이동 (.setLength (0) 통해)하여 재사용 할 수 있습니다. sb.toString은 또 다른 복사본 (불변의 char 배열)을 생성하므로 루프 당 하나의 새 버퍼 만 필요로하는 .setLength (0) 메서드와 달리 각 반복에는 두 개의 버퍼가 필요합니다.
Chris

25

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

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

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

1
항상 중괄호로 전체 범위를 지정할 수 있으므로 외부에 Stringbuilder가 없어도됩니다.
Epaga

@Epaga : 여전히 루프 자체 밖에 있습니다. 예, 외부 범위를 오염 시키지는 않지만 컨텍스트 에서 확인되지 않은 성능 향상을위한 코드를 작성하는 부 자연스러운 방법 입니다.
Jon Skeet

또는 더 나은 방법은 모든 것을 자체 방법에 넣는 것입니다. ;-) 그러나 나는 당신의 re : 문맥을 듣는다.
Epaga

임의의 숫자 합계 (4096) 대신 예상 크기로 초기화하는 것이 더 좋습니다. 코드는 크기 4096의 char []를 참조하는 문자열을 반환 할 수 있습니다 (JDK에 따라 다름; 내가 기억하는 한 1.4의 경우)
kohlerm

24

더 빠른 속도 :

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
이것이 가장 좋은 대답입니다. StringBuilder는 char 배열로 지원되며 변경 가능합니다. .toString ()이 호출되면 char 배열이 복사되고 변경 불가능한 문자열을 백업하는 데 사용됩니다. 이 시점에서 StringBuilder의 변경 가능한 버퍼는 삽입 포인터를 다시 0으로 이동 (.setLength (0) 통해)하여 재사용 할 수 있습니다. 루프 당 새로운 StringBuilder 할당을 제안하는 답변은 .toString이 또 다른 복사본을 생성한다는 것을 깨닫지 못하는 것 같습니다. 따라서 각 반복에는 루프 당 하나의 새 버퍼 만 필요한 .setLength (0) 메서드와는 반대로 두 개의 버퍼가 필요합니다.
Chris

12

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

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

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

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


.NET에 대한 몇 가지 재미있는 정보는 상황이 다릅니다. .NET StringBuilder는 내부적으로 일반 "문자열"개체를 수정하고 toString 메서드는 단순히 개체를 반환합니다 (수정 불가능으로 표시하므로 결과적으로 StringBuilder 조작으로 다시 생성됨). 따라서 일반적인 "new StringBuilder-> modify it-> to String"시퀀스는 추가 복사본을 만들지 않습니다 (결과 문자열 길이가 용량보다 훨씬 짧은 경우 저장소 확장 또는 축소에만 해당). Java에서이주기는 항상 적어도 하나의 복사본을 만듭니다 (StringBuilder.toString ()).
Ivan Dubrov

Sun JDK 1.5 이전 버전에는 예상
Dan Berindei

9

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

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

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

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


4

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


"사소하지 않은"것이 핵심 인 경우-벤치 마크는 한 형태가 비례 적으로 더 빠르다 는 것을 보여줄 수 있지만 실제 앱에서 얼마나 많은 시간이 소요되는지에 대한 힌트는 없습니다. :)
Jon Skeet

아래 내 대답의 벤치 마크를 참조하십시오. 두 번째 방법은 더 빠릅니다.
Epaga

1
@Epaga : 벤치 마크는 실제 앱의 성능 향상에 대해 거의 언급하지 않습니다. StringBuilder 할당을 수행하는 데 걸리는 시간은 나머지 루프에 비해 사소 할 수 있습니다. 이것이 벤치마킹에서 컨텍스트가 중요한 이유입니다.
Jon Skeet

1
@Epaga : 그가 실제 코드로 측정하기 전까지는 그것이 얼마나 중요한지 전혀 알 수 없습니다. 루프를 반복 할 때마다 많은 코드가 있다면 여전히 관련성이 없을 것이라고 강력하게 의심합니다. 우리는 "..."에 무엇이 있는지 모릅니다
Jon Skeet

1
(오해하지 마세요. btw-벤치 마크 결과는 그 자체로 여전히 매우 흥미 롭습니다. 저는 마이크로 벤치 마크에 매료되었습니다. 실제 테스트를 수행하기 전에 코드를 변형시키는 것을 좋아하지 않습니다.)
Jon Skeet

4

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

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


문제는 이전 루프 반복이 끝날 때 새로 생성 된 문자열이 기존 데이터를 사용하기 때문에 어쨌든 더 많은 메모리를 재 할당해야한다는 것입니다.
Jon Skeet

오, 그게 말이되는데 toString이 새 문자열 인스턴스를 할당하고 반환하고 빌더의 바이트 버퍼가 재 할당하는 대신 지워졌지만.
cfeduke

Epaga의 벤치 마크는 정리 및 재사용이 모든 패스에서 인스턴스화보다 이득임을 보여줍니다.
cfeduke

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 % 빠르다는 것을 보여줍니다.


1

LOL, 처음으로 사람들이 StringBuilder에서 문자열을 결합하여 성능을 비교하는 것을 본 적이 있습니다. 이를 위해 "+"를 사용하면 더 빠를 수 있습니다; D. StringBuilder를 사용하여 "지역성"개념으로 전체 문자열 검색 속도를 높이는 목적.

빈번한 변경이 필요하지 않은 문자열 값을 자주 검색하는 시나리오에서 Stringbuilder는 문자열 검색의 성능을 높일 수 있습니다. 그리고 그것이 Stringbuilder를 사용하는 목적입니다. 그 핵심 목적을 MIS-Test하지 마십시오 ..

어떤 사람들은 비행기가 더 빨리 날아 간다고 말했습니다. 따라서 자전거로 테스트 한 결과 비행기가 느리게 움직이는 것을 발견했습니다. 내가 실험 설정을 어떻게 설정하는지 아십니까; D


1

그다지 빠르지는 않지만 내 테스트에서 평균적으로 1.6.0_45 64 비트를 사용하여 몇 밀리 더 빠르다는 것을 보여줍니다 .StringBuilder.delete () 대신 StringBuilder.setLength (0)을 사용하십시오.

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

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 를 새로운 크기로 간단히 업데이트합니다 .


0

첫 번째는 인간에게 더 좋습니다. 두 번째가 일부 JVM의 일부 버전에서 조금 더 빠르다면 어떻게 될까요?

성능이 그렇게 중요하다면 StringBuilder를 우회하고 직접 작성하십시오. 당신이 훌륭한 프로그래머이고 당신의 앱이이 기능을 어떻게 사용하고 있는지 고려한다면, 당신은 더 빠르게 만들 수있을 것입니다. 할 보람 있는? 아마 아닐 것입니다.

이 질문이 "좋아하는 질문"으로 표시되는 이유는 무엇입니까? 성능 최적화는 실용적이든 아니든 매우 재미 있기 때문입니다.


학문적 질문 만이 아닙니다. 대부분의 경우 (95 % 읽기) 가독성과 유지 보수성을 선호하지만, 약간의 개선이 큰 차이를 만드는 경우가 있습니다.
Pier Luigi

네, 대답을 변경하겠습니다. 객체가 그것을 지우고 재사용 할 수있는 방법을 제공한다면 그렇게하십시오. 클리어가 효율적인지 확인하려면 먼저 코드를 조사하십시오. 아마도 그것은 개인 배열을 해제합니다! 효율적인 경우 객체를 루프 외부에 할당하고 내부에서 재사용하십시오.
dongilmore

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 msec (루프 내부 선언) 및 8236 msec (루프 외부 선언)

수십억 개의 루프가있는 주소 중복 제거 프로그램을 실행하더라도 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 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);

==> 3638msec (외부 루프), 2908msec (내부 루프)

감사합니다, Ulrich


-2

한 번 선언하고 매번 할당하십시오. 최적화보다 실용적이고 재사용 가능한 개념입니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.