Apakah lebih baik menggunakan kembali StringBuilder dalam satu lingkaran?

101

Saya memiliki pertanyaan terkait kinerja tentang penggunaan StringBuilder. Dalam loop yang sangat panjang saya memanipulasi StringBuilderdan meneruskannya ke metode lain seperti ini:

for (loop condition) {
    StringBuilder sb = new StringBuilder();
    sb.append("some string");
    . . .
    sb.append(anotherString);
    . . .
    passToMethod(sb.toString());
}

Apakah instantiating StringBuilderpada setiap siklus loop adalah solusi yang baik? Dan apakah memanggil delete lebih baik, seperti berikut ini?

StringBuilder sb = new StringBuilder();
for (loop condition) {
    sb.delete(0, sb.length);
    sb.append("some string");
    . . .
    sb.append(anotherString);
    . . .
    passToMethod(sb.toString());
}
Pier Luigi
sumber

Jawaban:

69

Yang kedua sekitar 25% lebih cepat dalam tolok ukur mini saya.

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

Hasil:

25265
17969

Perhatikan bahwa ini dengan JRE 1.6.0_07.


Berdasarkan ide Jon Skeet yang di edit, inilah versi 2. Hasil yang sama.

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

Hasil:

5016
7516
Epaga
sumber
4
Saya telah menambahkan suntingan dalam jawaban saya untuk menjelaskan mengapa ini mungkin terjadi. Saya akan melihat lebih hati-hati dalam beberapa saat (45 menit). Perhatikan bahwa melakukan penggabungan dalam panggilan append mengurangi tujuan penggunaan StringBuilder di tempat pertama :)
Jon Skeet
3
Juga akan menarik untuk melihat apa yang terjadi jika Anda membalikkan dua blok - JIT masih "memanaskan" StringBuilder selama pengujian pertama. Ini mungkin tidak relevan, tetapi menarik untuk dicoba.
Jon Skeet
1
Saya masih menggunakan versi pertama karena lebih bersih . Tetapi ada baiknya Anda benar-benar melakukan benchmark :) Perubahan yang disarankan berikutnya: coba # 1 dengan kapasitas yang sesuai diteruskan ke konstruktor.
Jon Skeet
25
Gunakan sb.setLength (0); sebaliknya, ini adalah cara tercepat untuk mengosongkan konten StringBuilder agar tidak membuat ulang objek atau menggunakan .delete (). Perhatikan bahwa ini tidak berlaku untuk StringBuffer, pemeriksaan konkurensinya membatalkan keunggulan kecepatan.
P Arrayah
1
Jawaban tidak efisien. P Arrayah dan Dave Jarvis benar. setLength (0) adalah jawaban yang paling efisien. StringBuilder didukung oleh array karakter dan bisa berubah. Pada titik .toString () dipanggil, array karakter disalin dan digunakan untuk mendukung string yang tidak dapat diubah. Pada titik ini, buffer yang dapat berubah dari StringBuilder dapat digunakan kembali, hanya dengan memindahkan pointer penyisipan kembali ke nol (melalui .setLength (0)). sb.toString membuat salinan lain (array karakter yang tidak dapat diubah), jadi setiap iterasi membutuhkan dua buffer sebagai lawan dari metode .setLength (0) yang hanya membutuhkan satu buffer baru per loop.
Chris
25

Dalam filosofi penulisan kode yang solid, selalu lebih baik untuk meletakkan StringBuilder Anda di dalam loop Anda. Dengan cara ini ia tidak keluar dari kode yang dimaksudkan.

Kedua, peningkatan terbesar di StringBuilder berasal dari memberikan ukuran awal untuk menghindarinya tumbuh lebih besar saat loop berjalan

for (loop condition) {
  StringBuilder sb = new StringBuilder(4096);
}
Peter
sumber
1
Anda selalu dapat mencakup semuanya dengan tanda kurung kurawal, dengan cara itu Anda tidak memiliki Stringbuilder di luar.
Epaga
@ Epaga: Ini masih di luar loop itu sendiri. Ya, itu tidak mencemari cakupan luar, tetapi ini adalah cara yang tidak wajar untuk menulis kode untuk peningkatan kinerja yang belum diverifikasi dalam konteks .
Jon Skeet
Atau bahkan lebih baik, taruh semuanya dalam metodenya sendiri. ;-) Tapi saya dengar ya re: konteks.
Epaga
Lebih baik lagi menginisialisasi dengan ukuran yang diharapkan daripada jumlah sewenang-wenang jumlah (4096) Kode Anda dapat mengembalikan String yang mereferensikan char [] dengan ukuran 4096 (tergantung pada JDK; sejauh yang saya ingat itu kasus untuk 1.4)
kohlerm
24

Lebih cepat lagi:

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

Dalam filosofi penulisan kode yang solid, cara kerja bagian dalam metode harus disembunyikan dari objek yang menggunakan metode tersebut. Jadi tidak ada bedanya dari perspektif sistem apakah Anda mendeklarasikan ulang StringBuilder di dalam atau di luar loop. Karena mendeklarasikannya di luar loop lebih cepat, dan tidak membuat kode lebih rumit untuk dibaca, maka gunakan kembali objek daripada memulihkannya.

Meskipun kodenya lebih rumit, dan Anda tahu pasti bahwa pembuatan instance objek adalah penghambat, beri komentar.

Tiga proses dengan jawaban ini:

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

Tiga proses dengan jawaban lain:

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

Meskipun tidak signifikan, pengaturan StringBuilderukuran buffer awal akan memberikan keuntungan yang kecil.

Dave Jarvis
sumber
3
Sejauh ini, ini adalah jawaban terbaik. StringBuilder didukung oleh array karakter dan bisa berubah. Pada titik .toString () dipanggil, array karakter disalin dan digunakan untuk mendukung string yang tidak dapat diubah. Pada titik ini, buffer yang dapat berubah dari StringBuilder dapat digunakan kembali, cukup dengan memindahkan pointer penyisipan kembali ke nol (melalui .setLength (0)). Jawaban tersebut menyarankan untuk mengalokasikan StringBuilder baru per loop tampaknya tidak menyadari bahwa .toString membuat salinan lain, jadi setiap iterasi memerlukan dua buffer sebagai lawan dari metode .setLength (0) yang hanya membutuhkan satu buffer baru per loop.
Chris
12

Oke, sekarang saya mengerti apa yang sedang terjadi, dan itu masuk akal.

Saya mendapat kesan bahwa toStringbaru saja melewati yang mendasari char[]ke dalam konstruktor String yang tidak mengambil salinan. Salinan kemudian akan dibuat pada operasi "tulis" berikutnya (misalnya delete). Saya yakin ini adalah kasus StringBufferdi beberapa versi sebelumnya. (Ini bukan sekarang.) Tapi tidak - toStringhanya meneruskan array (dan indeks dan panjang) ke Stringkonstruktor publik yang mengambil salinan.

Jadi dalam kasus "reuse the StringBuilder" kita benar-benar membuat satu salinan data per string, menggunakan array karakter yang sama di buffer sepanjang waktu. Jelas membuat baru StringBuildersetiap kali membuat buffer yang mendasari baru - dan kemudian buffer itu disalin (agak sia-sia, dalam kasus khusus kami, tetapi dilakukan untuk alasan keamanan) saat membuat string baru.

Semua ini mengarah pada versi kedua yang pasti lebih efisien - tetapi pada saat yang sama saya masih mengatakan itu kode yang lebih jelek.

Jon Skeet
sumber
Hanya beberapa info lucu tentang .NET, situasinya berbeda. .NET StringBuilder secara internal memodifikasi objek "string" biasa dan metode toString hanya mengembalikannya (menandainya sebagai tidak dapat dimodifikasi, sehingga manipulasi StringBuilder akibatnya akan membuatnya kembali). Jadi, urutan "baru StringBuilder-> modifikasi-> ke String" tidak akan membuat salinan tambahan (hanya untuk memperluas penyimpanan atau menyusutkannya, jika panjang string yang dihasilkan jauh lebih pendek daripada kapasitasnya). Di Java, siklus ini selalu membuat setidaknya satu salinan (di StringBuilder.toString ()).
Ivan Dubrov
Sun JDK pra-1.5 memiliki pengoptimalan yang Anda asumsikan: bugs.sun.com/bugdatabase/view_bug.do?bug_id=6219959
Dan Berindei
9

Karena saya rasa itu belum ditunjukkan, karena pengoptimalan yang dibangun ke dalam kompiler Java Sun, yang secara otomatis membuat StringBuilders (StringBuffers pra-J2SE 5.0) ketika melihat rangkaian String, contoh pertama dalam pertanyaan tersebut setara dengan:

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

Mana yang lebih mudah dibaca, IMO, pendekatan yang lebih baik. Upaya Anda untuk mengoptimalkan dapat menghasilkan keuntungan di beberapa platform, tetapi berpotensi kehilangan yang lain.

Tetapi jika Anda benar-benar mengalami masalah dengan kinerja, maka pastikan, optimalkan. Saya akan mulai dengan secara eksplisit menentukan ukuran buffer dari StringBuilder, menurut Jon Skeet.

Jack Leow
sumber
4

JVM modern benar-benar pintar tentang hal-hal seperti ini. Saya tidak akan menebak-nebak dan melakukan sesuatu yang hacky yang kurang dapat dipelihara / dibaca ... kecuali Anda melakukan benchmark yang tepat dengan data produksi yang memvalidasi peningkatan kinerja yang tidak sepele (dan mendokumentasikannya;)

Stu Thompson
sumber
Di mana "non-sepele" adalah kuncinya - tolok ukur dapat menunjukkan satu bentuk secara proporsional lebih cepat, tetapi tanpa petunjuk tentang berapa banyak waktu yang dibutuhkan dalam aplikasi yang sebenarnya :)
Jon Skeet
Lihat benchmark pada jawaban saya di bawah ini. Cara kedua lebih cepat.
Epaga
1
@ Epaga: Tolok ukur Anda tidak banyak menjelaskan tentang peningkatan kinerja di aplikasi nyata, di mana waktu yang dibutuhkan untuk melakukan alokasi StringBuilder mungkin lebih mudah dibandingkan dengan loop lainnya. Itulah mengapa konteks penting dalam benchmarking.
Jon Skeet
1
@ Epaga: Sampai dia mengukurnya dengan kode aslinya, kita tidak akan tahu seberapa signifikan itu sebenarnya. Jika ada banyak kode untuk setiap iterasi loop, saya sangat curiga itu masih tidak relevan. Kami tidak tahu apa yang ada di "..."
Jon Skeet
1
(Jangan salah paham, btw - hasil benchmark Anda masih sangat menarik. Saya terpesona oleh microbenchmark. Saya hanya tidak suka membengkokkan kode saya dari bentuknya sebelum melakukan tes kehidupan nyata juga.)
Jon Skeet
4

Berdasarkan pengalaman saya dengan mengembangkan perangkat lunak pada Windows, saya akan mengatakan membersihkan StringBuilder selama loop Anda memiliki kinerja yang lebih baik daripada membuat Instansiasi StringBuilder dengan setiap iterasi. Menghapusnya membebaskan memori itu untuk segera ditimpa tanpa alokasi tambahan yang diperlukan. Saya tidak cukup paham dengan pengumpul sampah Java, tapi menurut saya membebaskan dan tidak ada realokasi (kecuali string Anda berikutnya menumbuhkan StringBuilder) lebih bermanfaat daripada instantiasi.

(Pendapat saya bertentangan dengan apa yang disarankan orang lain. Hmm. Saatnya untuk mengukurnya.)

cfeduke
sumber
Masalahnya adalah bahwa lebih banyak memori harus dialokasikan kembali, karena data yang ada sedang digunakan oleh String yang baru dibuat di akhir iterasi loop sebelumnya.
Jon Skeet
Oh itu masuk akal, saya berpikir bahwa toString mengalokasikan dan mengembalikan instance string baru dan buffer byte untuk pembangun dihapus alih-alih mengalokasikan ulang.
cfeduke
Tolok ukur Epaga menunjukkan bahwa membersihkan dan menggunakan kembali adalah keuntungan atas instansiasi di setiap operan.
cfeduke
1

Alasan mengapa melakukan 'setLength' atau 'delete' meningkatkan kinerja sebagian besar adalah kode 'mempelajari' ukuran buffer yang tepat, dan lebih sedikit untuk melakukan alokasi memori. Secara umum, saya menyarankan agar kompilator melakukan optimasi string . Namun, jika kinerjanya kritis, saya akan sering menghitung sebelumnya ukuran buffer yang diharapkan. Ukuran StringBuilder default adalah 16 karakter. Jika Anda tumbuh lebih dari itu, maka ukurannya harus diubah. Mengubah ukuran adalah saat performa hilang. Berikut patokan mini lain yang menggambarkan hal ini:

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

Hasilnya menunjukkan penggunaan ulang objek sekitar 10% lebih cepat daripada membuat buffer dengan ukuran yang diharapkan.

brianegge.dll
sumber
1

LOL, pertama kali saya melihat orang membandingkan kinerja dengan menggabungkan string di StringBuilder. Untuk tujuan itu, jika Anda menggunakan "+", itu bisa lebih cepat; D. Tujuan menggunakan StringBuilder untuk mempercepat pengambilan seluruh string sebagai konsep "lokalitas".

Dalam skenario di mana Anda sering mengambil nilai String yang tidak perlu sering diubah, Stringbuilder memungkinkan kinerja pengambilan string yang lebih tinggi. Dan itulah tujuan menggunakan Stringbuilder .. tolong jangan MIS-Test tujuan intinya ..

Beberapa orang berkata, Pesawat terbang lebih cepat. Oleh karena itu, saya mengujinya dengan sepeda saya, dan ternyata pesawat bergerak lebih lambat. Tahukah Anda bagaimana saya mengatur pengaturan percobaan; D

Ting Choo Chiaw
sumber
1

Tidak secara signifikan lebih cepat, tetapi dari pengujian saya itu menunjukkan rata-rata menjadi beberapa mil lebih cepat menggunakan 1.6.0_45 64 bit: gunakan StringBuilder.setLength (0) daripada StringBuilder.delete ():

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 );
johnmartel
sumber
1

Cara tercepat adalah dengan menggunakan "setLength". Ini tidak akan melibatkan operasi penyalinan. Cara membuat StringBuilder baru harus benar-benar keluar . Lambatnya StringBuilder.delete (int start, int end) adalah karena ia akan menyalin array lagi untuk bagian yang mengubah ukuran.

 System.arraycopy(value, start+len, value, start, count-end);

Setelah itu, StringBuilder.delete () akan memperbarui StringBuilder.count ke ukuran baru. Sedangkan StringBuilder.setLength () hanya menyederhanakan update StringBuilder.count ke ukuran yang baru.

Shen liang
sumber
0

Yang pertama lebih baik untuk manusia. Jika yang kedua sedikit lebih cepat pada beberapa versi dari beberapa JVM, lalu apa?

Jika kinerja sangat penting, lewati StringBuilder dan tulis milik Anda sendiri. Jika Anda seorang programmer yang baik, dan mempertimbangkan bagaimana aplikasi Anda menggunakan fungsi ini, Anda seharusnya dapat membuatnya lebih cepat. Bermanfaat? Mungkin tidak.

Mengapa pertanyaan ini dipandang sebagai "pertanyaan favorit"? Karena pengoptimalan kinerja sangat menyenangkan, tidak peduli apakah itu praktis atau tidak.

dongilmore
sumber
Ini bukan pertanyaan akademis saja. Sementara sebagian besar waktu (baca 95%) saya lebih suka keterbacaan dan pemeliharaan, sebenarnya ada kasus di mana sedikit perbaikan membuat perbedaan besar ...
Pier Luigi
Oke, saya akan mengubah jawaban saya. Jika suatu objek menyediakan metode yang memungkinkannya untuk dibersihkan dan digunakan kembali, lakukanlah. Periksa kodenya terlebih dahulu jika Anda ingin memastikan pembersihan itu efisien; mungkin itu merilis array pribadi! Jika efisien, alokasikan objek di luar loop dan gunakan kembali di dalam.
dongilmore
0

Saya rasa tidak masuk akal untuk mencoba mengoptimalkan kinerja seperti itu. Hari ini (2019) kedua statment berjalan sekitar 11 detik untuk 100.000.000 loop di Laptop I5 saya:

    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 (deklarasi di dalam loop) dan 8236 msec (deklarasi loop luar)

Bahkan jika saya menjalankan program untuk pengurangan alamat dengan beberapa miliar pengulangan dengan selisih 2 detik. untuk 100 juta loop tidak ada bedanya karena program tersebut berjalan berjam-jam. Ketahuilah juga bahwa semuanya berbeda jika Anda hanya memiliki satu pernyataan tambahan:

    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 (loop dalam), 3555 msec (loop luar) Pernyataan pertama yang membuat StringBuilder dalam loop lebih cepat dalam kasus itu. Dan, jika Anda mengubah urutan eksekusi, ini jauh lebih cepat:

    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 msec (loop luar), 2908 msec (loop dalam)

Salam, Ulrich

Ulrich K.
sumber
-2

Nyatakan sekali, dan tetapkan setiap kali. Ini adalah konsep yang lebih pragmatis dan dapat digunakan kembali daripada pengoptimalan.

Peter Mortensen
sumber