루프에서 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.