Czy powinienem korzystać z String.format () Java, jeśli wydajność jest ważna?

215

Musimy cały czas budować ciągi dla danych wyjściowych dziennika i tak dalej. W wersjach JDK nauczyliśmy się, kiedy używać StringBuffer(wiele dodatków, bezpieczny wątek) i StringBuilder(wiele dodatków, nie bezpieczny wątek).

Jaka jest rada na temat korzystania String.format()? Czy jest wydajny, czy też jesteśmy zmuszeni trzymać się konkatenacji dla jedno-liniowych, w których wydajność jest ważna?

np. brzydki stary styl,

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

vs. schludny nowy styl (String.format, który jest prawdopodobnie wolniejszy),

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

Uwaga: moim szczególnym przypadkiem użycia są setki ciągów dziennika „jeden wiersz” w całym kodzie. Nie wymagają pętli, więc StringBuilderjest zbyt ciężki. Interesuje mnie String.format()konkretnie.

Powietrze
źródło
28
Dlaczego tego nie przetestujesz?
Ed S.
1
Jeśli tworzysz ten wynik, zakładam, że musi on być czytelny dla człowieka, tak jak człowiek może go odczytać. Powiedzmy, że najwyżej 10 linii na sekundę. Myślę, że przekonasz się, że to naprawdę nie ma znaczenia, jakie podejście wybierzesz, jeśli jest to teoretycznie wolniejsze, użytkownik może to docenić. ;) Więc nie, StringBuilder nie jest ciężki w większości sytuacji.
Peter Lawrey,
9
@Peter, nie, to absolutnie nie jest do czytania w czasie rzeczywistym przez ludzi! Ma to pomóc w analizie, gdy coś pójdzie nie tak. Dane wyjściowe dziennika zwykle będą wynosić tysiące linii na sekundę, więc muszą być wydajne.
Air
5
jeśli produkujesz wiele tysięcy wierszy na sekundę, sugerowałbym 1) użycie krótszego tekstu, nawet żadnego tekstu, takiego jak zwykły CSV lub plik binarny 2) Nie używaj w ogóle ciągu, możesz zapisać dane w ByteBuffer bez tworzenia wszelkie obiekty (tekstowe lub binarne) 3) w tle zapisują dane na dysk lub gniazdo. Powinieneś być w stanie utrzymać około 1 miliona linii na sekundę. (Zasadniczo tyle, ile pozwala podsystem dyskowy) Możesz uzyskać serię 10-krotności tego.
Peter Lawrey,
7
Nie dotyczy to ogólnego przypadku, ale w szczególności w przypadku logowania LogBack (napisany przez oryginalnego autora Log4j) ma formę sparametryzowanego rejestrowania, który rozwiązuje dokładnie ten problem - logback.qos.ch/manual/architecture.html#ParametrizedLogging
Matt Passell

Odpowiedzi:

123

Napisałem małą klasę do przetestowania, która ma lepszą wydajność dwóch i + wyprzedza format. od 5 do 6 razy. Spróbuj sam

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

    }
}

Wykonanie powyższego dla różnych N pokazuje, że oba zachowują się liniowo, ale String.formatsą 5-30 razy wolniejsze.

Powodem jest to, że w bieżącej implementacji String.formatnajpierw analizuje dane wejściowe za pomocą wyrażeń regularnych, a następnie wypełnia parametry. Z drugiej strony konkatenacja z plusem jest optymalizowana przez javac (nie przez JIT) i używana StringBuilder.appendbezpośrednio.

Porównanie czasu wykonywania

hhafez
źródło
12
Ten test ma jedną wadę, ponieważ nie jest w pełni dobrą reprezentacją całego formatowania łańcucha. Często jest logika związana z tym, co należy uwzględnić, i logika do formatowania określonych wartości w łańcuchy. Każdy prawdziwy test powinien uwzględniać scenariusze z prawdziwego świata.
Orion Adrian
9
Na SO było jeszcze pytanie o wersety + StringBuffer, w najnowszych wersjach Java +, gdy było to możliwe, zastąpiono StringBuffer, więc wydajność nie będzie inna
hhafez
25
Wygląda to bardzo podobnie do rodzaju mikrodruku, który zostanie zoptymalizowany w bardzo nieużyteczny sposób.
David H. Clements,
20
Kolejny źle wdrożony mikro-test porównawczy. Jak obie metody skalują się według rzędów wielkości. Co powiesz na użycie operacji 100, 1000, 10000, 1000000. Jeśli uruchomisz tylko jeden test, o jednym rzędzie wielkości, dla aplikacji, która nie działa na izolowanym rdzeniu; nie ma sposobu, aby powiedzieć, jaką różnicę można odrzucić jako „skutki uboczne” z powodu przełączania kontekstu, procesów w tle itp.
Evan Plaice
8
Co więcej, ponieważ nigdy nie wychodzisz z głównego JIT, nie możesz go uruchomić.
Jan Zyka
241

Wziąłem hhafez kod i dodał test pamięci :

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

Uruchamiam to osobno dla każdego podejścia, operatora „+”, String.format i StringBuilder (wzywając toString ()), więc inne podejścia nie będą miały wpływu na używaną pamięć. Dodałem więcej konkatenacji, tworząc ciąg jako „Blah” + i + „Blah” + i + „Blah” + i + „Blah”.

Rezultaty są następujące (średnio po 5 przebiegów):
Czas podejścia (ms) Przydzielona pamięć (długa)
operator „+” 747 320,504
String.format 16484 373,312
StringBuilder 769 57 344

Widzimy, że String „+” i StringBuilder są praktycznie identyczne pod względem czasu, ale StringBuilder jest znacznie wydajniejszy w użyciu pamięci. Jest to bardzo ważne, gdy mamy wiele wywołań dziennika (lub dowolnych innych instrukcji zawierających łańcuchy) w odstępie czasu wystarczająco krótkim, aby Garbage Collector nie był w stanie wyczyścić wielu instancji łańcucha wynikającego z operatora „+”.

I uwaga, BTW, nie zapomnij sprawdzić poziomu rejestrowania przed zbudowaniem wiadomości.

Wnioski:

  1. Będę nadal używać StringBuilder.
  2. Mam za dużo czasu lub za mało życia.
Itamar
źródło
8
„nie zapomnij sprawdzić poziomu rejestrowania przed zbudowaniem komunikatu”, to dobra rada, należy to zrobić przynajmniej w przypadku komunikatów debugowania, ponieważ może być ich dużo i nie powinny być włączone w środowisku produkcyjnym.
stivlo
39
Nie, to nie jest poprawne. Przepraszam, że jestem tępy, ale liczba pozytywnych opinii, które przyciągnęła, jest niepokojąca. Użycie +operatora kompiluje do równoważnego StringBuilderkodu. Mikrobenszety takie jak ten nie są dobrym sposobem mierzenia wydajności - dlaczego nie użyć jvisualvm, jest z jakiegoś powodu w jdk. String.format() będzie wolniejszy, ale ze względu na czas, aby przeanalizować ciąg formatu, a nie przydziały obiektów. Odroczenie tworzenia artefaktów rejestrowania, dopóki nie upewnisz się, że są one potrzebne, jest dobrą radą, ale jeśli miałoby to wpływ na wydajność, jest w niewłaściwym miejscu.
CurtainDog,
1
@CurtainDog, Twój komentarz został napisany na temat czteroletniego postu. Czy możesz wskazać dokumentację lub utworzyć osobną odpowiedź, aby zaradzić różnicy?
kurtzbot
1
Odniesienie na poparcie komentarza @ CurtainDog: stackoverflow.com/a/1532499/2872712 . Oznacza to, że + jest preferowane, chyba że odbywa się to w pętli.
morela
And a note, BTW, don't forget to check the logging level before constructing the message.nie jest dobra rada. Zakładając, że mówimy java.util.logging.*konkretnie, sprawdzanie poziomu rejestrowania ma miejsce, gdy mówisz o zaawansowanym przetwarzaniu, które spowodowałoby niekorzystne skutki dla programu, którego nie chciałbyś, gdy program nie ma włączonego rejestrowania na odpowiednim poziomie. Formatowanie łańcucha nie jest wcale tego rodzaju przetwarzaniem. Formatowanie jest częścią java.util.loggingframeworka, a sam program rejestrujący sprawdza poziom rejestrowania przed wywołaniem formatera.
searchengine27
30

Wszystkie przedstawione tutaj testy porównawcze mają pewne wady , dlatego wyniki nie są wiarygodne.

Byłem zaskoczony, że nikt nie używał JMH do testów porównawczych, więc zrobiłem to.

Wyniki:

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

Jednostki to operacje na sekundę, im więcej, tym lepiej. Kod źródłowy testu porównawczego . Użyto wirtualnej maszyny Java OpenJDK IcedTea 2.5.4.

Tak więc stary styl (używanie +) jest znacznie szybszy.

Adam Stelmaszczyk
źródło
5
Byłoby to o wiele łatwiejsze do zinterpretowania, jeśli adnotacja oznaczona była „+”, a która „formatem”.
AjahnCharles
21

Twój stary brzydki styl jest automatycznie kompilowany przez JAVAC 1.6 jako:

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

Więc nie ma absolutnie żadnej różnicy między tym a użyciem StringBuilder.

String.format ma dużo większą wagę, ponieważ tworzy nowy formatter, analizuje ciąg formatu wejściowego, tworzy StringBuilder, dołącza do niego wszystko i wywołuje metodęString ().

Raphaël
źródło
Pod względem czytelności opublikowany kod jest o wiele bardziej ... kłopotliwy niż String.format („Co otrzymujesz, jeśli pomnożysz% d przez% d?”, VarSix, varNine);
dusktreader
12
Nie ma różnicy między +i StringBuilderrzeczywiście. Niestety w innych odpowiedziach w tym wątku jest wiele dezinformacji. Niemal mam ochotę zmienić pytanie na how should I not be measuring performance.
CurtainDog,
12

String.format Java działa tak:

  1. analizuje ciąg formatu, rozbijając się na listę fragmentów formatu
  2. iteruje fragmenty formatu, renderując do StringBuilder, który jest w zasadzie tablicą, która zmienia się w razie potrzeby, kopiując do nowej tablicy. jest to konieczne, ponieważ nie wiemy jeszcze, jak duża jest alokacja końcowego ciągu
  3. StringBuilder.toString () kopiuje swój bufor wewnętrzny do nowego ciągu

jeśli ostatecznym miejscem docelowym dla tych danych jest strumień (np. renderowanie strony internetowej lub zapisywanie do pliku), możesz złożyć fragmenty formatu bezpośrednio w strumień:

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

Spekuluję, że optymalizator zoptymalizuje przetwarzanie ciągu formatu. Jeśli tak, pozostaje Ci tyle samo zamortyzowanej wydajności, co ręczne rozwijanie pliku String.format w StringBuilder.

Dustin Getz
źródło
5
Nie sądzę, aby twoje spekulacje na temat optymalizacji przetwarzania łańcucha formatu były prawidłowe. W niektórych rzeczywistych testach z wykorzystaniem Java 7 stwierdziłem, że używanie String.formatw wewnętrznych pętlach (uruchamianie milionów razy) skutkowało ponad 10% mojego czasu wykonywania java.util.Formatter.parse(String). Wydaje się to wskazywać, że w wewnętrznych pętlach należy unikać wywoływania Formatter.formatlub czegokolwiek, co go wywołuje, w tym PrintStream.format(wady standardowej biblioteki Java JO, IMO, zwłaszcza, że ​​nie można buforować analizowanego ciągu formatu).
Andy MacKinlay,
8

Aby rozwinąć / poprawić pierwszą odpowiedź powyżej, nie jest to tłumaczenie, w którym String.format faktycznie by pomógł.
Pomaga w tym String.format, kiedy drukujesz datę / godzinę (lub format numeryczny itp.), Gdzie występują różnice lokalizacyjne (1010) (tj. Niektóre kraje wydrukują 04 lutego 2009, a inne wydrukują 04 lutego 2009).
W tłumaczeniu mówisz tylko o przeniesieniu dowolnych eksternalizowalnych ciągów (takich jak komunikaty o błędach i nie-to) do pakietu właściwości, abyś mógł użyć odpowiedniego pakietu dla odpowiedniego języka, używając ResourceBundle i MessageFormat.

Patrząc na wszystkie powyższe, powiedziałbym, że pod względem wydajności String.format vs. zwykła konkatenacja sprowadza się do tego, co wolisz. Jeśli wolisz patrzeć na wywołania .format zamiast konkatenacji, to na pewno idź z tym.
W końcu kod jest odczytywany znacznie więcej niż jest napisany.

dw.mackie
źródło
1
Powiedziałbym, że pod względem wydajności String.format vs. zwykła konkatenacja sprowadza się do tego, co wolisz , myślę, że jest to nieprawidłowe. Pod względem wydajności konkatenacja jest znacznie lepsza. Aby uzyskać więcej informacji, zapoznaj się z moją odpowiedzią.
Adam Stelmaszczyk
6

W twoim przykładzie wydajność prawdopodobnie nie różni się zbytnio, ale należy wziąć pod uwagę inne kwestie: a mianowicie fragmentację pamięci. Nawet operacja konkatenacji tworzy nowy ciąg, nawet jeśli jest tymczasowy (potrzeba czasu, aby go GC i więcej pracy). String.format () jest po prostu bardziej czytelny i wymaga mniejszej fragmentacji.

Ponadto, jeśli często używasz określonego formatu, nie zapomnij, że możesz bezpośrednio użyć klasy Formatter () (wszystko, co robi String.format (), tworzy jedną instancję Formattera).

Ponadto należy pamiętać o czymś innym: należy uważać na użycie substring (). Na przykład:

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

Ten duży ciąg jest nadal w pamięci, ponieważ tak właśnie działają podciągi Java. Lepsza wersja to:

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

lub

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

Druga forma jest prawdopodobnie bardziej przydatna, jeśli robisz inne rzeczy w tym samym czasie.

Cletus
źródło
8
Warto wskazać, że „powiązane pytanie” to tak naprawdę C # i dlatego nie dotyczy.
Air
jakiego narzędzia użyłeś do zmierzenia fragmentacji pamięci i czy fragmentacja robi różnicę prędkości dla pamięci RAM?
kritzikratzi
Warto zauważyć, że zmieniono metodę podciągów z Java 7+. Powinien teraz zwrócić nową reprezentację typu String zawierającą tylko znaki podłańcuchowe. Oznacza to, że nie trzeba zwracać wywołania String :: new
João Rebelo
5

Ogólnie powinieneś używać String.Format, ponieważ jest stosunkowo szybki i obsługuje globalizację (zakładając, że faktycznie próbujesz napisać coś, co jest czytane przez użytkownika). Ułatwia także globalizację, jeśli próbujesz przetłumaczyć jeden ciąg w porównaniu do 3 lub więcej na instrukcję (szczególnie dla języków, które mają drastycznie różne struktury gramatyczne).

Teraz, jeśli nigdy nie planujesz niczego tłumaczyć, polegaj na wbudowanej w Javę konwersji operatorów + na język StringBuilder. Lub użyj StringBuilderjawnie Java .

Orion Adrian
źródło
3

Inna perspektywa tylko z punktu widzenia rejestrowania.

Widzę wiele dyskusji związanych z logowaniem się do tego wątku, więc pomyślałem o dodaniu mojego doświadczenia w odpowiedzi. Być może ktoś uzna to za przydatne.

Wydaje mi się, że motywacją do logowania przy użyciu formatera jest unikanie łączenia łańcuchów. Zasadniczo, nie chcesz mieć narzutu łańcucha konkat, jeśli nie zamierzasz go zalogować.

Tak naprawdę nie musisz konkatować / formatować, chyba że chcesz się zalogować. Powiedzmy, że jeśli zdefiniuję taką metodę

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

W tym podejściu cancat / formatter nie jest tak naprawdę wywoływany, jeśli jest to komunikat debugowania, a debugOn = false

Chociaż nadal lepiej będzie użyć StringBuilder zamiast formatera. Główną motywacją jest unikanie tego.

Jednocześnie od tego czasu nie lubię dodawać bloku „if” do każdej instrukcji logowania

  • Wpływa na czytelność
  • Zmniejsza zasięg moich testów jednostkowych - to mylące, gdy chcesz mieć pewność, że każda linia jest testowana.

Dlatego wolę utworzyć klasę narzędzia do rejestrowania za pomocą metod takich jak powyżej i używać jej wszędzie, nie martwiąc się o obniżenie wydajności i inne związane z tym problemy.

software.wikipedia
źródło
Czy możesz wykorzystać istniejącą bibliotekę, taką jak slf4j-api, która rzekomo zajmuje się tą sprawą dzięki sparametryzowanej funkcji rejestrowania? slf4j.org/faq.html#logging_performance
ammianus
2

Właśnie zmodyfikowałem test hhafeza, aby uwzględnić StringBuilder. StringBuilder jest 33 razy szybszy niż String.format przy użyciu klienta jdk 1.6.0_10 na XP. Użycie przełącznika -server obniża współczynnik do 20.

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

Chociaż może to zabrzmieć drastycznie, uważam, że ma to znaczenie tylko w rzadkich przypadkach, ponieważ liczby bezwzględne są dość niskie: 4 s na 1 milion prostych wywołań String.format jest w porządku - o ile używam ich do logowania lub lubić.

Aktualizacja: Jak wskazał sjbotha w komentarzach, test StringBuilder jest nieprawidłowy, ponieważ brakuje w nim wersji końcowej .toString().

Prawidłowy współczynnik przyspieszenia od String.format(.)do StringBuilderwynosi 23 na mojej maszynie (16 z -serverprzełącznikiem).

the.duckman
źródło
1
Twój test jest nieważny, ponieważ nie uwzględnia czasu zjedzonego przez samą pętlę. Powinieneś to uwzględnić i odjąć co najmniej od wszystkich innych wyników (tak, może to być znaczny procent).
cletus
Zrobiłem to, pętla for zajmuje 0 ms. Ale nawet jeśli zajęłoby to trochę czasu, zwiększyłoby to tylko ten czynnik.
the.duckman
3
Test StringBuilder jest nieprawidłowy, ponieważ na końcu nie wywołuje metody toString () w celu uzyskania ciągu, którego można użyć. Dodałem to, a wynik jest taki, że StringBuilder zajmuje tyle samo czasu co +. Jestem pewien, że wraz ze wzrostem liczby dodatków stanie się w końcu tańszy.
Sarel Botha
1

Oto zmodyfikowana wersja wpisu hhafez. Zawiera opcję konstruktora ciągów.

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

}

}

Czas po pętli 391 Czas po pętli 4163 Czas po pętli 227

ZARAZ
źródło
0

Odpowiedź zależy w dużej mierze od tego, w jaki sposób Twój kompilator Java optymalizuje generowany kod bajtowy. Ciągi są niezmienne i teoretycznie każda operacja „+” może utworzyć nową. Ale twój kompilator prawie na pewno optymalizuje tymczasowe kroki w budowaniu długich łańcuchów. Jest całkiem możliwe, że oba powyższe wiersze kodu generują dokładnie ten sam kod bajtowy.

Jedynym prawdziwym sposobem na sprawdzenie tego jest iteracyjne przetestowanie kodu w bieżącym środowisku. Napisz aplikację QD, która łączy ciągi w obie strony iteracyjnie, i zobacz, jak się ze sobą łączą.

Tak - ten Jake.
źródło
1
Kod bajtowy dla drugiego przykładu z pewnością wywołuje String.format, ale byłbym przerażony, gdyby zrobiła to prosta konkatenacja. Dlaczego kompilator miałby używać łańcucha formatu, który następnie musiałby zostać przeanalizowany?
Jon Skeet
Użyłem „kodu bajtowego”, w którym powinienem powiedzieć „kod binarny”. Gdy wszystko sprowadza się do jmps i movs, może to być dokładnie ten sam kod.
Tak - ten Jake.
0

Rozważ użycie "hello".concat( "world!" )niewielkiej liczby ciągów w konkatenacji. Wydajność może być jeszcze lepsza niż w przypadku innych podejść.

Jeśli masz więcej niż 3 łańcuchy, rozważ użycie StringBuilder lub po prostu String, w zależności od używanego kompilatora.

Sasa
źródło