Performans önemliyse Java'nın String.format () yöntemini kullanmalı mıyım?


215

Günlük çıkışı için her zaman Dizeler oluşturmalıyız vb. JDK sürümlerinde ne zaman kullanılacağını StringBuffer(birçok ek, iş parçacığı güvenli) ve StringBuilder(birçok ek , iş parçacığı için güvenli değil) öğrendik .

Kullanmayla ilgili tavsiye nedir String.format()? Verimli midir, yoksa performansın önemli olduğu tek gömlekler için birleştirme işlemine devam etmek zorunda mıyız?

örneğin çirkin eski tarz,

String s = "What do you get if you multiply " + varSix + " by " + varNine + "?";

düzenli yeni stil (muhtemelen daha yavaş olan String.format),

String s = String.format("What do you get if you multiply %d by %d?", varSix, varNine);

Not: Özel kullanım durumum, kodum boyunca yüzlerce 'tek satırlı' günlük dizesidir. Bir döngü içermezler, bu yüzden StringBuilderçok ağırdır. String.format()Özellikle ilgileniyorum .


28
Neden test etmiyorsun?
Ed S.

1
Bu çıktıyı üretiyorsanız, o zaman bir insanın okuyabileceği bir oran olarak bir insan tarafından okunabilir olması gerektiğini varsayıyorum. En fazla saniyede 10 satır söyleyelim. Bence hangi yaklaşımı aldığınız önemli değil, eğer daha yavaşsa, kullanıcı bunu takdir edebilir. ;) Yani hayır, StringBuilder çoğu durumda ağır değildir.
Peter Lawrey

9
@Peter, hayır kesinlikle insanlar tarafından gerçek zamanlı olarak okumak için değil! İşler ters gittiğinde analize yardımcı olmak için orada. Günlük çıktısı genellikle saniyede binlerce satır olacaktır, bu nedenle verimli olması gerekir.
hava durumu

5
Saniyede binlerce satır üretiyorsanız, 1) daha kısa metin kullanın, düz CSV veya ikili gibi bir metin bile kullanmayın 2) String kullanmayın, verileri oluşturmadan bir ByteBuffer'a yazabilirsiniz herhangi bir nesne (metin veya ikili olarak) 3) verinin diske veya sokete yazılmasını sağlar. Saniyede yaklaşık 1 milyon satır sürdürebilmelisiniz. (Temel olarak disk alt sisteminizin izin verdiği ölçüde) Bunun 10 katı kadar patlama yapabilirsiniz.
Peter Lawrey

7
Bu genel durumla ilgili değildir, ancak özellikle günlük kaydı için LogBack (orijinal Log4j yazarı tarafından yazılmıştır) bu tam sorunu gideren bir parametreli günlük kaydı biçimine sahiptir - logback.qos.ch/manual/architecture.html#ParametrizedLogging
Matt Passell

Yanıtlar:


123

Test etmek için küçük bir sınıf yazdım, bu ikisi daha iyi performansa sahip ve + formattan önce geliyor. 5 ile 6 arasında.

import java.io.*;
import java.util.Date;

public class StringTest{

    public static void main( String[] args ){
    int i = 0;
    long prev_time = System.currentTimeMillis();
    long time;

    for( i = 0; i< 100000; i++){
        String s = "Blah" + i + "Blah";
    }
    time = System.currentTimeMillis() - prev_time;

    System.out.println("Time after for loop " + time);

    prev_time = System.currentTimeMillis();
    for( i = 0; i<100000; i++){
        String s = String.format("Blah %d Blah", i);
    }
    time = System.currentTimeMillis() - prev_time;
    System.out.println("Time after for loop " + time);

    }
}

Yukarıdakileri farklı N için çalıştırmak, her ikisinin de doğrusal davrandığını, ancak String.format5-30 kat daha yavaş olduğunu gösterir.

Bunun nedeni, mevcut uygulamada String.formatönce girdiyi düzenli ifadelerle ayrıştırması ve daha sonra parametreleri doldurmasıdır. Öte yandan, artı ile birleştirme javac tarafından optimize edilir (JIT tarafından değil) ve StringBuilder.appenddoğrudan kullanılır.

Çalışma zamanı karşılaştırması


12
Bu sınamanın bir kusuru var, çünkü tüm dize biçimlendirmesinin tamamen iyi bir temsili değil. Genellikle belirli değerleri dizelere biçimlendirmek için nelerin dahil edileceği ve mantıkla ilgili bir mantık vardır. Herhangi bir gerçek test, gerçek dünya senaryolarına bakmalıdır.
Orion Adrian

9
SO hakkında + ayetleri StringBuffer hakkında başka bir soru vardı, Java + 'nın son sürümlerinde mümkün olduğunda StringBuffer ile değiştirildi, bu yüzden performans farklı olmayacaktı
hhafez

25
Bu, çok kullanışsız bir şekilde optimize edilecek olan mikrobenchmark türüne çok benziyor.
David H. Clements

20
Kötü uygulanmış başka bir mikro kriter. Her iki yöntem de büyüklük derecelerine göre nasıl ölçeklenir. 100, 1000, 10000, 1000000, işlemleri kullanmaya ne dersiniz? Yalıtılmış bir çekirdek üzerinde çalışmayan bir uygulamada yalnızca bir büyüklük sırasına göre tek bir test çalıştırırsanız; bağlam değiştirme, arka plan işlemleri
Evan Plaice


241

Aldığım hhafez kodu ve bir katma bellek testi :

private static void test() {
    Runtime runtime = Runtime.getRuntime();
    long memory;
    ...
    memory = runtime.freeMemory();
    // for loop code
    memory = memory-runtime.freeMemory();

Bu ayrı ayrı her yaklaşım, '+' operatörü, String.format ve StringBuilder (toString () çağrılıyor) için çalıştırın, böylece kullanılan bellek diğer yaklaşımlardan etkilenmez. Dizeyi "Blah" + i + "Blah" + i + "Blah" + i + "Blah" olarak yaparak daha fazla birleştirme ekledim.

Sonuç aşağıdaki gibidir (her biri ortalama 5 çalışma):
Yaklaşma Süresi (ms) Tahsis edilen bellek (uzun)
'+' operatörü 747 320,504
String.format 16484 373,312
StringBuilder 769 57,344

String '+' ve StringBuilder'ın pratikte aynı özdeş olduklarını görebiliriz, ancak StringBuilder bellek kullanımında çok daha verimlidir. Bu, Çöp Toplayıcı '+' operatörünün neden olduğu birçok dize örneğini temizleyemeyecek kadar kısa bir sürede çok sayıda günlük çağrımız (veya dizelerle ilgili herhangi bir deyimimiz) olduğunda çok önemlidir.

Ve bir not, BTW, mesajı oluşturmadan önce günlük seviyesini kontrol etmeyi unutmayın .

Sonuç:

  1. StringBuilder'ı kullanmaya devam edeceğim.
  2. Çok fazla zamanım ya da çok az hayatım var.

8
"Mesajı kurmadan önce kayıt seviyesini kontrol etmeyi unutmayın", iyi bir tavsiye, bu en azından hata ayıklama mesajları için yapılmalıdır, çünkü bunların birçoğu olabilir ve üretimde etkinleştirilmemelidir.
stivlo

39
Hayır, bu doğru değil. Açıkçası özür dilerim ama çektiği upvotes sayısı endişe verici bir şey değil. Kullanılması +operatörü eşdeğer derler StringBuilderkodu. Bunun gibi mikrobenzerler performansı ölçmenin iyi bir yolu değildir - neden jvisualvm kullanmıyorsunuz, bir nedenden dolayı jdk'de. String.format() daha yavaş olacaktır , ancak herhangi bir nesne ayırması yerine biçim dizesini ayrıştırma süresi nedeniyle. Eğer onlar ihtiyaç var emin olana kadar eserler giriş oluşturulmasını erteleniyor olan iyi tavsiye, ama bir performans etkisini olacaktır eğer yanlış yerde.
CurtainDog

1
@CurtainDog, yorumunuz dört yaşında bir yayına yapıldı, belgelere işaret edebilir veya farkı ele almak için ayrı bir cevap oluşturabilir misiniz?
kurtzbot

1
@ CurtainDog'un yorumunu destekleyen referans: stackoverflow.com/a/1532499/2872712 . Yani, bir döngü içinde yapılmadığı sürece + tercih edilir.
kayısı

And a note, BTW, don't forget to check the logging level before constructing the message.iyi bir tavsiye değil. java.util.logging.*Özel olarak konuştuğumuzu varsayarsak , kayıt düzeyini kontrol etmek, bir programın günlük kaydı uygun seviyeye gelmediğinde istemediğiniz bir program üzerinde olumsuz etkilere neden olacak gelişmiş işleme yapmaktan bahsediyor olmanızdır. Dize biçimlendirmesi AT ALL gibi bir işlem türü değildir. Biçimlendirme java.util.loggingçerçevenin bir parçasıdır ve kaydedicinin kendisi biçimlendirici çağrılmadan önce günlük düzeyini denetler.
searchengine27

30

Burada sunulan tüm kriterler bazı kusurlara sahiptir , bu nedenle sonuçlar güvenilir değildir.

Kıyaslama için kimsenin JMH kullanmadığına şaşırdım , bu yüzden yaptım.

Sonuçlar:

Benchmark             Mode  Cnt     Score     Error  Units
MyBenchmark.testOld  thrpt   20  9645.834 ± 238.165  ops/s  // using +
MyBenchmark.testNew  thrpt   20   429.898 ±  10.551  ops/s  // using String.format

Birimler saniyedeki işlemlerdir, daha iyi olur. Karşılaştırma kaynak kodu . OpenJDK IcedTea 2.5.4 Java Sanal Makinesi kullanıldı.

Yani, eski stil (+ kullanarak) çok daha hızlı.


5
Hangisinin "+" ve hangisinin "biçim" olduğunu eklediyseniz, yorumlanması çok daha kolay olurdu.
AjahnCharles

21

Eski çirkin tarzınız JAVAC 1.6 tarafından otomatik olarak şu şekilde derlenir:

StringBuilder sb = new StringBuilder("What do you get if you multiply ");
sb.append(varSix);
sb.append(" by ");
sb.append(varNine);
sb.append("?");
String s =  sb.toString();

Yani bu ve bir StringBuilder kullanma arasında kesinlikle bir fark yoktur.

String.format çok daha ağırdır, çünkü yeni bir Formatter oluşturur, giriş formatı dizenizi ayrıştırır, bir StringBuilder oluşturur, her şeyi ona ekler ve toString () öğesini çağırır.


Okunabilirlik açısından, yayınladığınız kod String.format'tan çok daha hantaldır ("% d ile% d ile çarptığınızda ne elde edersiniz?", VarSix, varNine);
dusktreader

12
Gerçekten +ve arasında fark yok StringBuilder. Ne yazık ki bu konudaki diğer cevaplarda çok fazla yanlış bilgi var. Soruyu değiştirmek için neredeyse cazip oluyorum how should I not be measuring performance.
CurtainDog

12

Java'nın String.format şu şekilde çalışır:

  1. biçim dizesini ayrıştırır ve biçim parçaları listesinde patlar
  2. yeni bir diziye kopyalayarak temelde gerektiği gibi kendini yeniden boyutlandıran bir dizi olan StringBuilder'a dönüştürerek biçim yığınlarını yineler. bu gereklidir, çünkü henüz son String'i ne kadar tahsis edeceğimizi bilmiyoruz
  3. StringBuilder.toString () dahili arabelleğini yeni bir String'e kopyalar

bu veriler için son hedef bir akışsa (örn. bir web sayfası oluşturma veya bir dosyaya yazma), biçim yığınlarını doğrudan akışınıza monte edebilirsiniz:

new PrintStream(outputStream, autoFlush, encoding).format("hello {0}", "world");

Optimize edicinin biçim dizesi işlemeyi optimize edeceğini tahmin ediyorum. Öyleyse, String.format'ınızı bir StringBuilder'a manuel olarak açmak için eşdeğer amortisman performansına sahip olursunuz.


5
Ben dize işleme biçimi optimizasyonu hakkında spekülasyon doğru olduğunu sanmıyorum. Java 7'yi kullanan bazı gerçek dünya testlerinde String.format, iç döngülerde (milyonlarca kez çalışan) kullanmanın , harcanan yürütme süremin% 10'undan fazlasıyla sonuçlandığını buldum java.util.Formatter.parse(String). Bu, iç döngülerde, aramadan Formatter.formatveya onu çağıran herhangi bir şeyden kaçınmanız gerektiği anlamına gelir PrintStream.format(Java'nın standart lib, IMO'daki bir kusur, özellikle ayrıştırılmış biçim dizesini önbelleğe alamadığınız için).
Andy MacKinlay

8

Yukarıdaki ilk cevabı genişletmek / düzeltmek, String.format'ın aslında yardımcı olacağı bir çeviri değildir.
String.format'ın yardımcı olacağı şey, yerelleştirme (l10n) farklılıklarının olduğu bir tarih / saat (veya sayısal biçim, vb.) Yazdırırken (yani, bazı ülkeler 04Feb2009 yazdıracak, diğerleri ise Feb042009 yazdıracaktır).
Çeviri ile, ResourceBundle ve MessageFormat'ı kullanarak doğru dil için doğru paketi kullanabilmeniz için, herhangi bir haricilaştırılabilir dizeyi (hata iletileri ve ne gibi) bir özellik paketine taşımaktan bahsediyorsunuz.

Yukarıdakilere baktığımda, performans açısından, String.format ve düz birleştirmenin tercih ettiğiniz şeye indiğini söyleyebilirim. Birleştirme üzerinden .format çağrılarına bakmayı tercih ediyorsanız, elbette bununla devam edin.
Sonuçta, kod yazıldığından çok daha fazla okunur.


1
Ben performans-bilge, String.format vs düz birleştirme tercih ettiğiniz yere geldiğini söyleyebilirim ben bunun yanlış olduğunu düşünüyorum. Performans açısından, birleştirme çok daha iyidir. Daha fazla bilgi için lütfen cevabıma bir göz atın.
Adam Stelmaszczyk

6

Örneğinizde, performans probalby çok farklı değildir, ancak dikkate alınması gereken başka konular da vardır: yani bellek parçalanması. Birleştirme işlemi bile geçici olsa bile yeni bir dize oluşturuyor (GC'ye geçmek zaman alıyor ve daha fazla iş). String.format () sadece daha okunabilir ve daha az parçalanma içerir.

Ayrıca, belirli bir biçimi çok kullanıyorsanız, Formatter () sınıfını doğrudan kullanabileceğinizi unutmayın (tüm String.format (), bir kullanımlı Formatter örneğini başlatır).

Ayrıca, bilmeniz gereken başka bir şey: substring () kullanmaya dikkat edin. Örneğin:

String getSmallString() {
  String largeString = // load from file; say 2M in size
  return largeString.substring(100, 300);
}

Bu büyük dize hala bellekte çünkü Java alt dizeleri böyle çalışıyor. Daha iyi bir sürüm:

  return new String(largeString.substring(100, 300));

veya

  return String.format("%s", largeString.substring(100, 300));

Aynı anda başka şeyler yapıyorsanız ikinci form muhtemelen daha kullanışlıdır.


8
"İlgili soru" işaret değer aslında C # ve bu nedenle geçerli değildir.
Air

bellek parçalanmasını ölçmek için hangi aracı kullandınız ve parçalanma koç için bir hız farkı yaratıyor mu?
kritzikratzi

Alt dize yönteminin Java 7 + 'dan değiştirildiğini belirtmek gerekir. Artık yalnızca alt dize karakterlerini içeren yeni bir Dize temsili döndürmelidir. Bu, çağrıyı geri döndürmeye gerek olmadığı anlamına gelir String :: new
João Rebelo

5

Genellikle String.Format'ı kullanmalısınız çünkü nispeten hızlıdır ve küreselleşmeyi destekler (aslında kullanıcı tarafından okunan bir şey yazmaya çalıştığınızı varsayarsak). Ayrıca, bir dizeyi ifade başına 3 veya daha fazlaya çevirmeye çalışıyorsanız (özellikle büyük ölçüde farklı gramer yapılarına sahip diller için) globalleşmeyi kolaylaştırır.

Eğer hiçbir şeyi tercüme etmeyi planlamıyorsanız, ya Java'nın + operatörlerinin içine dönüştürülmesine güvenin StringBuilder. Veya Java'yı StringBuilderaçıkça kullanın .


3

Yalnızca Günlükleme açısından başka bir perspektif.

Bu konuda oturum açma ile ilgili çok fazla tartışma görüyorum, bu yüzden cevabımı deneyimime eklemeyi düşündüm. Birisi faydalı bulabilir.

Sanırım biçimlendiriciyi kullanarak günlüğe kaydetme motivasyonu dize birleştirme kaçınmak geliyor. Temel olarak, günlüğe kaydetmeyecekseniz dize concat yükü istemezsiniz.

Oturum açmak istemediğiniz sürece gerçekten birleştirme / biçimlendirme yapmanız gerekmez. Diyelim ki böyle bir yöntem tanımlarsam

public void logDebug(String... args, Throwable t) {
    if(debugOn) {
       // call concat methods for all args
       //log the final debug message
    }
}

Bu yaklaşımda cancat / formatter gerçekten çağrılmazsa bir hata ayıklama mesajı ve debugOn = false

Yine de burada biçimlendirici yerine StringBuilder kullanmak daha iyi olacaktır. Ana motivasyon bunlardan kaçınmaktır.

Aynı zamanda her günlük ifadesi için "if" bloğu eklemeyi sevmiyorum

  • Okunabilirliği etkiler
  • Birim testlerimin kapsamını azaltır - bu, her hattın test edildiğinden emin olmak istediğinizde kafa karıştırıcıdır.

Bu nedenle, yukarıdaki gibi yöntemlerle bir günlük yardımcı programı sınıfı oluşturmayı ve performans isabetini ve bununla ilgili diğer sorunları düşünmeden her yerde kullanmayı tercih ederim.


Bu kullanıcı tabanını parametreli günlük kaydı özellikleriyle ele alan, slf4j-api gibi varolan bir kitaplıktan yararlanabilir misiniz? slf4j.org/faq.html#logging_performance
ammianus

2

Hhafez'in testini StringBuilder'ı içerecek şekilde değiştirdim. StringBuilder, XP'de jdk 1.6.0_10 istemcisini kullanarak String.format'tan 33 kat daha hızlıdır. -Server anahtarını kullanmak faktörü 20'ye düşürür.

public class StringTest {

   public static void main( String[] args ) {
      test();
      test();
   }

   private static void test() {
      int i = 0;
      long prev_time = System.currentTimeMillis();
      long time;

      for ( i = 0; i < 1000000; i++ ) {
         String s = "Blah" + i + "Blah";
      }
      time = System.currentTimeMillis() - prev_time;

      System.out.println("Time after for loop " + time);

      prev_time = System.currentTimeMillis();
      for ( i = 0; i < 1000000; i++ ) {
         String s = String.format("Blah %d Blah", i);
      }
      time = System.currentTimeMillis() - prev_time;
      System.out.println("Time after for loop " + time);

      prev_time = System.currentTimeMillis();
      for ( i = 0; i < 1000000; i++ ) {
         new StringBuilder("Blah").append(i).append("Blah");
      }
      time = System.currentTimeMillis() - prev_time;
      System.out.println("Time after for loop " + time);
   }
}

Bu kulağa sert gelse de, mutlak sayılar oldukça düşük olduğu için sadece nadir durumlarda alakalı olduğunu düşünüyorum: 1 milyon basit String için 4 s.format aramaları tamam - kayıt için kullandığım sürece sevmek.

Güncelleme: Yorumlarda sjbotha tarafından belirtildiği gibi, bir final eksik olduğu için StringBuilder testi geçersiz .toString().

Doğru hızlandırıcı faktör String.format(.)için StringBuilderbenim makinede (16 23 olan -serveranahtarı).


1
Testiniz geçersizdir, çünkü sadece bir döngü ile tüketilen zamanı dikkate almaz. Bunu dahil etmeli ve en azından diğer sonuçlardan çıkarmalısınız (evet, önemli bir yüzde olabilir).
cletus

Bunu yaptım, for döngüsü 0 ms sürer. Ancak zaman alsa bile, bu sadece faktörü artıracaktır.
the.duckman

3
StringBuilder testi geçersiz çünkü aslında size kullanabileceğiniz bir String vermek için toString () öğesini çağırmıyor. Bunu ekledim ve sonuç StringBuilder + ile yaklaşık aynı miktarda zaman alır. Eminim ekleme sayısını artırdıkça sonunda daha ucuz hale gelecektir.
Sarel Botha

1

İşte hhafez girdisinin değiştirilmiş versiyonu. Bir dize oluşturucu seçeneği içerir.

public class BLA
{
public static final String BLAH = "Blah ";
public static final String BLAH2 = " Blah";
public static final String BLAH3 = "Blah %d Blah";


public static void main(String[] args) {
    int i = 0;
    long prev_time = System.currentTimeMillis();
    long time;
    int numLoops = 1000000;

    for( i = 0; i< numLoops; i++){
        String s = BLAH + i + BLAH2;
    }
    time = System.currentTimeMillis() - prev_time;

    System.out.println("Time after for loop " + time);

    prev_time = System.currentTimeMillis();
    for( i = 0; i<numLoops; i++){
        String s = String.format(BLAH3, i);
    }
    time = System.currentTimeMillis() - prev_time;
    System.out.println("Time after for loop " + time);

    prev_time = System.currentTimeMillis();
    for( i = 0; i<numLoops; i++){
        StringBuilder sb = new StringBuilder();
        sb.append(BLAH);
        sb.append(i);
        sb.append(BLAH2);
        String s = sb.toString();
    }
    time = System.currentTimeMillis() - prev_time;
    System.out.println("Time after for loop " + time);

}

}

Döngü 391 için zaman sonra Döngü 4163 için zaman sonra Döngü 227 için zaman sonra


0

Bunun cevabı, belirli Java derleyicinizin oluşturduğu bayt kodunu nasıl optimize ettiğine bağlıdır. Dizeler değişmezdir ve teorik olarak her "+" işlemi yeni bir tane oluşturabilir. Ancak, derleyiciniz uzun dizeler oluşturmak için ara adımları neredeyse kesinlikle optimize eder. Yukarıdaki her iki kod satırının da aynı bayt kodunu oluşturması tamamen mümkündür.

Bilmenin tek gerçek yolu, kodu mevcut ortamınızda yinelemeli olarak test etmektir. Dizeleri yinelemeli olarak her iki şekilde birleştiren bir QD uygulaması yazın ve birbirlerine karşı zaman aşımlarını görün.


1
İkinci örnek için bayt kodu kesinlikle String.format çağırır, ancak basit bir birleştirme yaptıysanız dehşete olurdu. Derleyici neden daha sonra ayrıştırılması gereken bir biçim dizesi kullanır?
Jon Skeet

"İkili kod" demem gereken "baytkodu" kullandım. Her şey jmps ve movs'a geldiğinde, aynı kod olabilir.
Evet - şu Jake.

0

"hello".concat( "world!" )Birleştirmede az sayıda dize kullanmayı düşünün . Performans için diğer yaklaşımlardan daha iyi olabilir.

3'ten fazla dizeniz varsa, kullandığınız derleyiciye bağlı olarak StringBuilder veya yalnızca String kullanmayı düşünün.

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.