web-dev-qa-db-ja.com

StringBuilderをループで再利用する方が良いでしょうか?

StringBuilderの使用に関するパフォーマンス関連の質問があります。非常に長いループでは、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());
}
96
Pier Luigi

2つ目は、ミニベンチマークで約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
67
Epaga

堅実なコードを書くという哲学では、StringBuilderをループ内に配置する方が常に優れています。この方法では、意図したコードの範囲外になりません。

次に、StringBuilderの最大の改善点は、ループの実行中にサイズが大きくなるのを防ぐために初期サイズを指定することです。

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

さらに高速:

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を再宣言しても、システムの観点から違いはありません。ループ外で宣言する方が高速であり、コードの読み取りが複雑になることはないため、オブジェクトを再インスタンス化するのではなく再利用します。

コードがより複雑で、オブジェクトのインスタンス化がボトルネックであることを確信していても、コメントしてください。

この答えで3つの実行:

$ Java ScratchPad
1567
$ Java ScratchPad
1569
$ Java ScratchPad
1570

他の答えと3つの実行:

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

重要ではありませんが、StringBuilderの初期バッファーサイズを設定すると、わずかなゲインが得られます。

23
Dave Jarvis

さて、私は今何が起こっているのか理解しており、それは理にかなっています。

toStringが基礎となるchar[]did n'tコピーを取得するStringコンストラクタに追加します。次に、次の「書き込み」操作でコピーが作成されます(例:delete)。私はこれを信じるwas以前のバージョンのStringBufferの場合。 (現在はそうではありません。)しかし-toStringは、配列(およびインデックスと長さ)を、コピーを取得するパブリックStringコンストラクターに渡すだけです。

したがって、「StringBuilderを再利用」する場合、バッファー内の同じchar配列を常に使用して、文字列ごとにデータのコピーを1つ作成します。明らかに新しいStringBuilderを作成するたびに新しい基礎となるバッファーが作成されます-そして、そのバッファーは新しい文字列を作成するときにコピーされます(このケースでは多少無意味ですが、安全上の理由で行われます)。

これにより、2番目のバージョンは間違いなくより効率的になりますが、同時にstillいコードであるとも言えます。

12
Jon Skeet

Sun Javaコンパイラに組み込まれた最適化により、まだ指摘されているとは思わないので、文字列の連結を検出したときにStringBuilder(StringBuffers J2SE 5.0より前)を自動的に作成します。質問の例は次と同等です:

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

より読みやすい、IMO、より良いアプローチ。最適化を試みると、一部のプラットフォームで利益が得られる可能性がありますが、他のプラットフォームでは損失が生じる可能性があります。

しかし、実際にパフォーマンスの問題に直面している場合は、必ず最適化してください。ただし、Jon Skeetごとに、StringBuilderのバッファーサイズを明示的に指定することから始めます。

9
Jack Leow

最新のJVMは、このようなものについて本当に賢いです。私はそれを二度と推測せず、保守性や可読性が低いハッキングを行います...些細なパフォーマンスの改善を検証する生産データで適切なベンチマークを行わない限り(そしてそれを文書化します;)

4
Stu Thompson

Windowsでソフトウェアを開発した経験に基づいて、ループ中にStringBuilderをクリアすると、反復ごとにStringBuilderをインスタンス化するよりもパフォーマンスが向上します。それをクリアすると、追加の割り当てを必要とせずに、そのメモリがすぐに上書きされます。 Javaガベージコレクターについて十分な知識はありませんが、インスタンス化よりも、次の文字列がStringBuilderを大きくしない限り)解放して再割り当てを行わない方が有益だと思います。

(私の意見は、他の皆が提案していることに反しています。うーん。それをベンチマークする時間です。)

4
cfeduke

最速の方法は「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を新しいサイズに更新します。

1
Shen liang

「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
brianegge

LOL、私が初めて見た人は、StringBuilderで文字列を組み合わせてパフォーマンスを比較しました。そのために、「+」を使用すると、さらに高速になる可能性があります; D。 StringBuilderを使用して、「ローカリティ」の概念として文字列全体の取得を高速化する目的。

頻繁に変更する必要のない文字列値を頻繁に取得するシナリオでは、Stringbuilderにより文字列取得のパフォーマンスが向上します。それがStringbuilderを使用する目的です。その中核的な目的をMISテストしないでください。

一部の人々は、飛行機はより速く飛ぶと言いました。そのため、自転車でテストしましたが、飛行機の動きが遅いことがわかりました。実験設定の設定方法を知っていますか; D

1
Ting Choo Chiaw

それほど高速ではありませんが、私のテストでは、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
johnmartel

残念ながら、私は通常C#を使用しています。StringBuilderをクリアすることは、新しいインスタンスをインスタンス化することよりも重要だと思います。

0

最初の方法は人間にとってより良い方法です。いくつかのJVMの一部のバージョンで2番目のほうが少し速い場合、それではどうしますか?

パフォーマンスがそれほど重要な場合は、StringBuilderをバイパスして独自のコードを作成してください。あなたが優れたプログラマーであり、アプリがこの関数をどのように使用しているかを考慮すると、さらに高速にできるはずです。価値がある?おそらくない。

なぜこの質問が「お気に入りの質問」として見られるのですか?パフォーマンスの最適化は実用的であるかどうかに関係なく、とても楽しいものです。

0
dongilmore