Bir StringBuilder'ı döngüde yeniden kullanmak daha mı iyi?


101

StringBuilder kullanımıyla ilgili performansla ilgili bir sorum var. Çok uzun bir döngüde a'yı değiştiriyorum StringBuilderve bunun gibi başka bir yönteme geçiriyorum:

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

StringBuilderHer döngü döngüsünde örnekleme yapmak iyi bir çözüm mü? Ve aşağıdaki gibi bir silme işlemini çağırmak daha mı iyi?

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

Yanıtlar:


69

İkincisi, mini karşılaştırmamda yaklaşık% 25 daha hızlı.

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

Sonuçlar:

25265
17969

Bunun JRE 1.6.0_07 ile olduğunu unutmayın.


Jon Skeet'in düzenlemedeki fikirlerine dayanarak, işte sürüm 2. Yine de aynı sonuçlar.

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

Sonuçlar:

5016
7516

4
Bunun neden olabileceğini açıklamak için cevabıma bir düzenleme ekledim. Bir süre sonra daha dikkatli bakacağım (45 dakika). Ekleme çağrılarında birleştirme yapmanın, StringBuilder'ı ilk etapta kullanma noktasını biraz azalttığını unutmayın :)
Jon Skeet

3
Ayrıca, iki bloğu ters çevirirseniz ne olacağını görmek ilginç olacaktır - JIT, ilk test sırasında StringBuilder'ı hala "ısınıyor". Alakasız olabilir ama denemek ilginç olabilir.
Jon Skeet

1
Daha temiz olduğu için yine de ilk versiyonu tercih ederim . Ama kıyaslamayı gerçekten yapmış olmanız iyi :) Bir sonraki önerilen değişiklik: kurucuya uygun bir kapasite geçirilerek # 1'i deneyin.
Jon Skeet

25
Sb.setLength (0) kullanın; bunun yerine, nesneyi yeniden oluşturmaya veya .delete () kullanmaya karşı StringBuilder içeriğini boşaltmanın en hızlı yoludur. Bunun StringBuffer için geçerli olmadığını unutmayın, eşzamanlılık kontrolleri hız avantajını geçersiz kılar.
P Arrayah

1
Verimsiz cevap. P Arrayah ve Dave Jarvis haklı. setLength (0) uzak ve uzak en etkili cevaptır. StringBuilder bir karakter dizisi tarafından desteklenir ve değiştirilebilir. .ToString () çağrıldığında, char dizisi kopyalanır ve değişmez bir dizeyi desteklemek için kullanılır. Bu noktada, StringBuilder'ın değiştirilebilir arabelleği, basitçe ekleme işaretçisini sıfıra geri getirerek (.setLength (0) aracılığıyla) yeniden kullanılabilir. sb.toString yine başka bir kopya (değişmez karakter dizisi) oluşturur, bu nedenle her yineleme, döngü başına yalnızca bir yeni arabellek gerektiren .setLength (0) yönteminin aksine iki arabellek gerektirir.
Chris

25

Katı kod yazma felsefesinde, StringBuilder'ınızı döngünüzün içine koymak her zaman daha iyidir. Bu şekilde amaçlandığı kodun dışına çıkmaz.

İkinci olarak, StringBuilder'daki en büyük iyileştirme, döngü çalışırken büyümesini önlemek için ona bir başlangıç ​​boyutu vermekten gelir.

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

1
Her şeyi her zaman küme parantezleri ile kapsamlandırabilirsiniz, bu şekilde Stringbuilder dışarıda olmaz.
Epaga

@Epaga: Hala döngünün dışında. Evet, dış kapsamı kirletmez, ancak bağlamda doğrulanmamış bir performans iyileştirmesi için kod yazmanın doğal olmayan bir yoludur .
Jon Skeet

Daha da iyisi, her şeyi kendi yöntemine göre koyun. ;-) Ama seni duydum: bağlam.
Epaga

Daha da iyisi, rasgele toplam sayı (4096) yerine beklenen boyutla
başlatın Kodunuz

24

Daha hızlı:

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

Katı kod yazma felsefesinde, yöntemin iç işleyişi, yöntemi kullanan nesnelerden gizlenmelidir. Bu nedenle, StringBuilder'ı döngü içinde veya döngü dışında yeniden beyan edip etmediğiniz, sistem açısından hiçbir fark yaratmaz. Döngünün dışında bildirmek daha hızlı olduğundan ve kodu okumayı daha karmaşık hale getirmediğinden, nesneyi yeniden başlatmak yerine yeniden kullanın.

Kod daha karmaşık olsa ve nesne somutlaştırmanın darboğaz olduğunu kesin olarak bilseniz bile, onu yorumlayın.

Bu cevapla üç çalışma:

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

Diğer cevapla üç koşu:

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

Önemli olmasa da, StringBuilder'ın başlangıçtaki arabellek boyutunu ayarlamak küçük bir kazanç sağlayacaktır.


3
Bu açık ara en iyi cevap. StringBuilder bir karakter dizisi tarafından desteklenir ve değiştirilebilir. .ToString () çağrıldığında, char dizisi kopyalanır ve değişmez bir dizeyi desteklemek için kullanılır. Bu noktada, StringBuilder'ın değiştirilebilir arabelleği, basitçe ekleme işaretçisini sıfıra geri hareket ettirerek (.setLength (0) aracılığıyla) yeniden kullanılabilir. Döngü başına yepyeni bir StringBuilder ayırmayı öneren bu yanıtlar, .toString'in başka bir kopya oluşturduğunun farkında değil gibi görünüyor, bu nedenle her yineleme, döngü başına yalnızca bir yeni arabellek gerektiren .setLength (0) yönteminin aksine iki arabellek gerektirir.
Chris

12

Tamam, şimdi neler olduğunu anlıyorum ve bu mantıklı.

Bir kopya almayan bir String yapıcısına toStringtemel alınan izlenim altındaydım . Daha sonra bir sonraki "yazma" işleminde bir kopya yapılır (örn. ). Ben buna inanıyorum oldu durum bazı önceki sürümde. (Şimdi değil.) Ama hayır - sadece diziyi (ve indeksi ve uzunluğu) bir kopya alan genel kurucuya aktarır.char[]deleteStringBuffertoStringString

Dolayısıyla, "yeniden kullanma StringBuilder" durumunda, her dizge için verinin gerçekten bir kopyasını, tampondaki tüm zaman boyunca aynı karakter dizisini kullanarak oluştururuz. Açıktır ki, StringBuilderher seferinde yeni bir tane oluşturmak yeni bir temel tampon oluşturur - ve sonra bu tampon, yeni bir dizge oluştururken kopyalanır (bizim özel durumumuzda bir şekilde anlamsız bir şekilde, ancak güvenlik nedenleriyle yapılır).

Bütün bunlar ikinci versiyonun kesinlikle daha verimli olmasına yol açar - ama aynı zamanda daha çirkin bir kod olduğunu söyleyebilirim.


NET hakkında biraz komik bilgi var, durum farklı. .NET StringBuilder, normal "string" nesnesini dahili olarak değiştirir ve toString yöntemi onu basitçe döndürür (onu değiştirilemez olarak işaretleyerek, dolayısıyla StringBuilder düzenlemeleri onu yeniden oluşturur). Bu nedenle, tipik "yeni StringBuilder-> modifiye et-> String'e" dizisi fazladan bir kopya yapmayacaktır (yalnızca, sonuçta ortaya çıkan dizi uzunluğu kapasitesinden çok daha kısaysa, depolamayı genişletmek veya küçültmek için). Java'da bu döngü her zaman en az bir kopya oluşturur (StringBuilder.toString () içinde).
Ivan Dubrov

Sun JDK pre-1.5, varsaydığınız optimizasyona sahipti: bugs.sun.com/bugdatabase/view_bug.do?bug_id=6219959
Dan Berindei

9

Henüz belirtilmediğini düşündüğüm için, String birleştirmelerini gördüğünde otomatik olarak StringBuilders (StringBuffers pre-J2SE 5.0) oluşturan Sun Java derleyicisinde yerleşik optimizasyonlar nedeniyle, sorudaki ilk örnek şuna eşdeğerdir:

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

Hangisi daha okunaklı, IMO, daha iyi yaklaşım. Optimize etme girişimleriniz bazı platformlarda kazançlarla sonuçlanabilir, ancak potansiyel olarak diğerlerini kaybedebilir.

Ancak gerçekten performansla ilgili sorun yaşıyorsanız, o zaman kesinlikle optimize edin. Jon Skeet'e göre StringBuilder'ın arabellek boyutunu açıkça belirterek başlayacağım.


4

Modern JVM, bunun gibi şeyler konusunda gerçekten akıllı. Önemsiz olmayan bir performans iyileştirmesini doğrulayan (ve belgeleyen;) üretim verileriyle doğru kıyaslama yapmazsanız, ikinci kez tahmin edip daha az bakımı / okunabilir bir şey yapmam.


"Önemsiz olmayan" anahtar kelime olduğunda - kıyaslamalar bir formun orantılı olarak daha hızlı olduğunu gösterebilir , ancak gerçek uygulamada ne kadar zaman aldığına dair hiçbir ipucu yoktur :)
Jon Skeet

Aşağıdaki cevabımdaki karşılaştırmaya bakın. İkinci yol daha hızlıdır.
Epaga

1
@Epaga: Karşılaştırma noktanız gerçek uygulamadaki performans iyileştirmesi hakkında çok az şey söylüyor, burada StringBuilder tahsisini yapmak için geçen zaman döngünün geri kalanına kıyasla önemsiz olabilir. Bu nedenle kıyaslamada bağlam önemlidir.
Jon Skeet

1
@Epaga: Gerçek koduyla ölçünceye kadar, bunun ne kadar önemli olduğuna dair hiçbir fikrimiz olmayacak. Döngünün her yinelemesi için çok fazla kod varsa, bunun hala alakasız olacağından şüpheleniyorum. "..." içinde ne olduğunu bilmiyoruz
Jon Skeet

1
(Beni yanlış anlamayın, btw - kıyaslama sonuçlarınız kendi içlerinde hala çok ilginç. Mikro testler beni büyülüyor. Sadece gerçek hayat testlerini gerçekleştirmeden önce kodumu şekilden bükmekten hoşlanmıyorum.)
Jon Skeet

4

Windows'ta yazılım geliştirme konusundaki deneyimime dayanarak, döngünüz sırasında StringBuilder'ı temizlemenin, her yinelemede bir StringBuilder'ı başlatmaya göre daha iyi performansa sahip olduğunu söyleyebilirim. Bunun temizlenmesi, bu hafızanın herhangi bir ek tahsis gerekmeden hemen üzerine yazılmasını sağlar. Java çöp toplayıcısına yeterince aşina değilim, ancak serbest bırakmanın ve yeniden tahsis etmemenin (bir sonraki dizeniz StringBuilder'ı büyütmedikçe) somutlaştırmadan daha yararlı olduğunu düşünüyorum.

(Benim fikrim herkesin önerdiğine aykırı. Hmm. Kıyaslama zamanı.)


Mesele şu ki, mevcut veriler önceki döngü yinelemesinin sonunda yeni oluşturulan String tarafından kullanıldığından, yine de daha fazla belleğin yeniden tahsis edilmesi gerekiyor.
Jon Skeet

Ah bu mantıklı, toString'in yeni bir dize örneği ayırıp geri döndürdüğünü ve kurucu için bayt tamponunun yeniden tahsis etmek yerine temizlediğini düşündüm.
cfeduke

Epaga'nın kıyaslaması, takas ve yeniden kullanmanın her geçişte somutlaştırmaya göre bir kazanç olduğunu gösteriyor.
cfeduke

1

Bir 'setLength' veya 'delete' yapmanın performansı iyileştirmesinin nedeni, çoğunlukla kodun doğru tampon boyutunu 'öğrenmesidir' ve bellek ayırma işlemini daha az yapmaktır. Genel olarak, derleyicinin dize optimizasyonlarını yapmasına izin vermenizi öneririm . Bununla birlikte, performans kritikse, genellikle beklenen tampon boyutunu önceden hesaplarım. Varsayılan StringBuilder boyutu 16 karakterdir. Bunun ötesine geçerseniz, yeniden boyutlandırılması gerekir. Yeniden boyutlandırma, performansın kaybolduğu yerdir. İşte bunu gösteren başka bir mini kriter:

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

Sonuçlar, nesnenin yeniden kullanılmasının beklenen boyutta bir arabellek oluşturmaya göre yaklaşık% 10 daha hızlı olduğunu göstermektedir.


1

LOL, ilk kez insanları StringBuilder'da dizeleri birleştirerek performansı karşılaştırdığımı gördüm. Bu amaçla, "+" kullanırsanız, daha da hızlı olabilir; D. StringBuilder'ı "yerellik" kavramı olarak tüm dizginin geri alınmasını hızlandırmak için kullanmanın amacı.

Sık sık değişiklik gerektirmeyen bir String değerini sık sık aldığınız senaryoda, Stringbuilder daha yüksek performans dizisi alma sağlar. Ve Stringbuilder kullanmanın amacı budur .. lütfen bunun temel amacını MIS-Test etmeyin ..

Bazıları, Uçak daha hızlı uçar dedi. Bu yüzden bisikletimle test ettim ve uçağın daha yavaş hareket ettiğini buldum. Deney ayarlarını nasıl yaptığımı biliyor musunuz; D


1

Çok daha hızlı değil, ancak testlerimden ortalama olarak 1.6.0_45 64 bit kullanarak birkaç milisaniye daha hızlı olduğunu gösteriyor: StringBuilder.delete () yerine StringBuilder.setLength (0) kullanın:

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

En hızlı yol "setLength" kullanmaktır. Kopyalama işlemini içermeyecek. Yeni bir StringBuilder oluşturmanın yolu tamamen çıkmış olmalıdır . StringBuilder.delete (int start, int end) için yavaş olan, diziyi yeniden boyutlandırma kısmı için tekrar kopyalayacağı içindir.

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

Bundan sonra, StringBuilder.delete (), StringBuilder.count'u yeni boyuta güncelleyecektir. StringBuilder.setLength () yalnızca StringBuilder.count'u yeni boyuta güncellemeyi basitleştirir .


0

Birincisi insanlar için daha iyidir. İkincisi, bazı JVM'lerin bazı sürümlerinde biraz daha hızlıysa ne olacak?

Performans bu kadar kritikse, StringBuilder'ı atlayın ve kendiniz yazın. İyi bir programcıysanız ve uygulamanızın bu işlevi nasıl kullandığını hesaba katarsanız, bunu daha da hızlı hale getirebilmelisiniz. Değerli? Muhtemelen değil.

Bu soru neden "favori soru" olarak görünüyor? Çünkü performans optimizasyonu pratik olsun ya da olmasın çok eğlenceli.


Bu sadece akademik bir soru değil. Çoğu zaman (% 95'i okuyun) Okunabilirliği ve sürdürülebilirliği tercih etsem de, gerçekten küçük iyileştirmelerin büyük farklar yarattığı durumlar var ...
Pier Luigi

Tamam, cevabımı değiştireceğim. Bir nesne, silinmesine ve yeniden kullanılmasına izin veren bir yöntem sağlıyorsa, bunu yapın. Netliğin verimli olduğundan emin olmak istiyorsanız önce kodu inceleyin; belki özel bir dizi yayınlar! Verimliyse, nesneyi döngünün dışında tahsis edin ve içinde yeniden kullanın.
dongilmore

0

Performansı bu şekilde optimize etmeye çalışmanın mantıklı olduğunu düşünmüyorum. Bugün (2019) her iki durum da I5 Dizüstü Bilgisayarımda 100.000.000 döngü için yaklaşık 11 saniye çalışıyor:

    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 ms (döngü içinde bildirim) ve 8236 ms (döngü dışında bildirim)

Birkaç milyar döngü ile adres tekilleştirme programları çalıştırıyor olsam bile 2 saniye farkla. 100 milyon döngü için herhangi bir fark yaratmaz çünkü bu programlar saatlerce çalışır. Ayrıca, yalnızca bir ek ifadeniz varsa, her şeyin farklı olduğunu unutmayın:

    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 milisaniye (iç döngü), 3555 milisaniye (dış döngü) Döngü içinde StringBuilder'ı oluşturan ilk ifade bu durumda daha hızlıdır. Ve yürütme sırasını değiştirirseniz, çok daha hızlıdır:

    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 milisaniye (dış döngü), 2908 milisaniye (iç döngü)

Saygılarımızla, Ulrich


-2

Bir kez beyan edin ve her seferinde atayın. Optimizasyondan daha pragmatik ve yeniden kullanılabilir bir kavramdır.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.