104

StringBuilder の使用に関するパフォーマンス関連の質問があります。非常に長いループで、次のStringBuilderように a を操作して別のメソッドに渡します。

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

14 に答える 14

70

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
于 2008-10-28T07:17:45.183 に答える
26

さらに速く:

public class ScratchPad {

    private static String a;

    public static void main( String[] args ) throws Exception {
        final long time = System.currentTimeMillis();

        // Pre-allocate enough space to store all appended strings.
        // StringBuilder, ultimately, uses an array of characters.
        final 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( final 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メモリの再割り当てを防ぐためにの初期バッファサイズを設定すると、パフォーマンスがわずかに向上します。

于 2009-06-22T06:20:33.010 に答える
25

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

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

for (loop condition) {
  StringBuilder sb = new StringBuilder(4096);
}
于 2008-10-28T07:14:30.150 に答える
12

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

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

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

これらすべてにより、2 番目のバージョンの方が効率的であることは間違いありませんが、同時に、より醜いコードであるとも言えます。

于 2008-10-28T08:18:59.460 に答える
9

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

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

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

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

于 2008-10-30T13:07:51.450 に答える
6

最新の JVM は、このようなことについて非常にスマートです。私はそれを二度と推測せず、保守性/可読性が低いハックなことをします...重要なパフォーマンスの改善を検証する本番データを使用して適切なベンチマークを実行しない限り(そしてそれを文書化します;)

于 2008-10-28T07:12:29.093 に答える
4

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

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

于 2008-10-28T07:17:24.777 に答える
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 );
于 2013-06-05T13:53:05.123 に答える
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% 高速であることを示しています。

于 2009-07-16T01:22:08.923 に答える
1

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

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

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

于 2010-12-17T05:05:00.933 に答える
0

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

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

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

于 2008-10-30T00:32:23.457 に答える
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);

==> 11000msec(ループ内宣言)、8236msec(ループ外宣言)

数十億回のループでアドレス重複排除用のプログラムを実行している場合でも、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);

==> 3638msec(ループ外)、2908msec(ループ内)

よろしく、ウルリッヒ

于 2019-10-22T11:41:30.150 に答える
-2

一度宣言し、毎回割り当てます。これは、最適化よりも実用的で再利用可能な概念です。

于 2008-10-28T13:52:05.153 に答える