ループで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

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でのことです。


編集中のジョン・スキートのアイデアに基づいて、ここにバージョン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
また、2つのブロックを逆にするとどうなるかを確認するのも興味深いでしょう。JITは、最初のテスト中にStringBuilderをまだ「ウォームアップ」しています。無関係かもしれませんが、試してみるのは興味深いでしょう。
Jon Skeet、

1
それはよりきれいなので、私はまだ最初のバージョンを使います。しかし、実際にベンチマークを実行したのは良いことです:)次に推奨される変更:コンストラクターに渡された適切な容量で#1を試してください。
Jon Skeet、

25
sb.setLength(0);を使用します。代わりに、オブジェクトの再作成または.delete()の使用に対してStringBuilderの内容を空にする最も速い方法です。これはStringBufferには適用されないことに注意してください。その並行性チェックは速度の利点を無効にします。
P Arrayah 2008

1
非効率的な答え。P ArrayahとDave Jarvisは正しいです。setLength(0)が最も効率的な答えです。StringBuilderはchar配列によってサポートされ、変更可能です。.toString()が呼び出された時点で、char配列がコピーされ、不変文字列をバックアップするために使用されます。この時点で、(。setLength(0)を使用して)挿入ポインタをゼロに戻すだけで、StringBuilderの可変バッファを再利用できます。sb.toStringはさらに別のコピー(不変のchar配列)を作成するため、ループごとに1つの新しいバッファーしか必要としない.setLength(0)メソッドとは対照的に、各反復には2つのバッファーが必要です。
クリス、

25

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

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

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

1
あなたはいつも中括弧で全体をスコープすることができます、それであなたは外部にStringbuilderを持っていません。
Epaga

@エパガ:それはまだループの外にあります。はい、それは外側のスコープを汚染しませんが、コンテキストで検証さていないパフォーマンス改善のためのコードを書くのは不自然な方法です。
Jon Skeet、

またはさらに良いことに、すべてを独自の方法で実行します。;-)しかし、re:コンテキストを聞きます。
Epaga、2008年

任意の数値の合計ではなく、予想されるサイズでより適切に初期化する(4096)コードは、サイズ4096のchar []を参照する文字列を返す場合があります(JDKによって異なりますが、1.4の場合と
同じでした

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

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

この答えで3つの実行:

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

他の答えで3つの実行:

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

重要ではありませんが、StringBuilderの初期バッファサイズを設定すると、小さな利益が得られます。


3
これは断然最良の答えです。StringBuilderはchar配列によってサポートされ、変更可能です。.toString()が呼び出された時点で、char配列がコピーされ、不変文字列をバックアップするために使用されます。この時点で、(。setLength(0)を使用して)挿入ポインタをゼロに戻すだけで、StringBuilderの可変バッファを再利用できます。ループごとに新しいStringBuilderを割り当てることを示唆するこれらの答えは、.toStringがさらに別のコピーを作成することを認識していないように思われるため、ループごとに1つの新しいバッファーのみを必要とする.setLength(0)メソッドとは対照的に、各反復には2つのバッファーが必要です。
クリス

12

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

コピーを取らない StringコンストラクターにtoString基礎char[]を渡しただけの印象を受けました。次に、次の「書き込み」操作(など)でコピーが作成されます。これ、以前の一部のバージョンの場合と同じであったと思います。(現在ではありません。)しかし、いいえ- 配列(およびインデックスと長さ)を、コピーを取るパブリックコンストラクターに渡します。deleteStringBuffertoStringString

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

これにより、2番目のバージョンの方が明らかに効率的ですが、それでも醜いコードだと私は言います。


.NETに関する面白い情報がいくつかあります。状況は異なります。.NET StringBuilderは通常の「文字列」オブジェクトを内部的に変更し、toStringメソッドはそれを単に返します(変更不可としてマークするため、その後のStringBuilder操作により再作成されます)。したがって、典型的な「新しいStringBuilder-> modify it-> to String」シーケンスは、余分なコピーを作成しません(結果の文字列の長さがその容量よりもはるかに短い場合に、ストレージを拡張または縮小する場合のみ)。Javaでは、このサイクルは常に少なくとも1つのコピーを作成します(StringBuilder.toString()内)。
Ivan Dubrov

Sun JDK 1.5より前のバージョンで
Dan Berindei

9

まだ指摘されていないと思うので、Sun Javaコンパイラに組み込まれた最適化により、String連結が検出されると自動的にStringBuilders(J2SE 5.0より前のStringBuffers)が作成されるため、質問の最初の例は次のようになります。

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

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

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


4

最近のJVMは、このようなものについては本当に賢いです。私はそれを2番目に推測せず、保守/読み取りが難しいハックなことをします...重要なパフォーマンス改善を検証する本番データで適切なベンチマークを実行しない限り(そしてそれを文書化します;)


「自明ではない」が重要な場合-ベンチマークでは、1つのフォームが比例して高速であることを示すことができますが、実際のアプリでどれだけ時間がかかっているかについてのヒントはありません:)
Jon Skeet

以下の私の答えのベンチマークを参照してください。2番目の方法はより高速です。
Epaga

1
@Epaga:実際のアプリのパフォーマンス改善についてベンチマークはほとんど語っていません。StringBuilderの割り当てにかかる時間は、ループの残りの部分と比較すると取るに足らないものです。そのため、ベンチマークではコンテキストが重要です。
Jon Skeet、

1
@エパガ:彼が実際のコードでそれを測定するまで、それが実際にどれほど重要であるかはわかりません。ループの反復ごとに多くのコードがある場合、私はそれがまだ無関係であることを強く疑っています。「...」の内容がわからない
Jon Skeet、

1
(誤解しないでください、ところで-ベンチマークの結果はそれでも非常に興味深いものです。マイクロベンチマークに魅了されます。実際のテストを実行する前に、コードを無理に曲げたくありません。)
ジョンスキート

4

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

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


とにかく、前のループ反復の最後に新しく作成された文字列が既存のデータを使用しているため、とにかくより多くのメモリを再割り当てする必要があります。
Jon Skeet、

ああ、それは理にかなっていますが、toStringが新しい文字列インスタンスを割り当てて返し、ビルダーのバイトバッファーが再割り当てではなくクリアされていたのですが。
cfeduke、2008年

Epagaのベンチマークは、クリアと再利用がすべてのパスでのインスタンス化よりも優れていることを示しています。
cfeduke、2008年

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で文字列を組み合わせてパフォーマンスを比較したのを見たことがあります。そのため、「+」を使用すると、さらに高速になる可能性があります。「局所性」の概念として、文字列全体の検索を高速化するためにStringBuilderを使用する目的。

頻繁な変更を必要としないString値を頻繁に取得するシナリオでは、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

1つ目は人間に適しています。一部のJVMの一部のバージョンで2番目のほうが少し速い場合は、どうでしょうか。

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

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


それは学術的な質問だけではありません。ほとんどの場合(95%を読む)読みやすさと保守性を好みますが、実際には少しの改善で大きな違いが生じる場合があります...
Pier Luigi

はい、答えを変更します。オブジェクトがクリアして再利用できるメソッドを提供している場合は、そのようにします。クリアが効率的であることを確認したい場合は、最初にコードを調べてください。多分それはプライベート配列をリリースします!効率的であれば、オブジェクトをループの外側に割り当て、それを内側で再利用します。
dongilmore 2008年

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ミリ秒(ループ内の宣言)および8236ミリ秒(ループ外の宣言)

私が数十億ループのアドレス重複除去のために2秒の差でプログラムを実行している場合でも。1億ループの場合、プログラムは何時間も実行されるため、何の違いもありません。また、appendステートメントが1つしかない場合は状況が異なることに注意してください。

    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ミリ秒(ループ内)、3555ミリ秒(ループ外)ループ内で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);

==> 3638ミリ秒(外側ループ)、2908ミリ秒(内側ループ)

よろしく、Ulrich


弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.