wake-up-neo.com

Sollte ich Javas String.format () verwenden, wenn die Leistung wichtig ist?

Wir müssen die ganze Zeit Strings für die Protokollausgabe erstellen und so weiter. In den JDK-Versionen haben wir gelernt, wann StringBuffer (viele Anhänge, threadsicher) und StringBuilder (viele Anhänge, nicht threadsicher) zu verwenden sind.

Was ist der Rat zur Verwendung von String.format()? Ist es effizient oder müssen wir uns an die Verkettung von Einzeilern halten, bei denen Leistung wichtig ist?

z.B. hässliche alte art,

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

ordentlich neuer Stil (String.format, möglicherweise langsamer),

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

Hinweis: Mein spezieller Anwendungsfall sind die Hunderte von "einzeiligen" Protokollzeichenfolgen in meinem Code. Da es sich nicht um eine Schleife handelt, ist StringBuilder zu schwergewichtig. Ich interessiere mich speziell für String.format().

203
Air

Ich habe eine kleine Klasse zum Testen geschrieben, die die bessere Leistung der beiden hat und + dem Format voraus ist. um den Faktor 5 bis 6. Probieren Sie es aus

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

    }
}

Wenn Sie das Obige für verschiedene N ausführen, zeigen Sie, dass sich beide linear verhalten, aber String.format ist 5-30 mal langsamer.

Der Grund ist, dass in der aktuellen Implementierung String.format analysiert zuerst die Eingabe mit regulären Ausdrücken und füllt dann die Parameter aus. Verkettung mit Plus hingegen wird von Javac (nicht vom JIT) optimiert und verwendet StringBuilder.append direkt.

Runtime comparison

119
hhafez

Ich nahm hhafez Code und fügte einen Speichertest hinzu:

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

Ich führe dies für jeden Ansatz separat aus, den Operator '+', String.format und StringBuilder (Aufruf von toString ()), damit der verwendete Speicher nicht von anderen Ansätzen beeinflusst wird. Ich habe weitere Verkettungen hinzugefügt und die Zeichenfolge zu "Blah" + i + "Blah" + i + "Blah" + i + "Blah" gemacht.

Das Ergebnis ist wie folgt (Durchschnitt von jeweils 5 Läufen):
Annäherungszeit (ms) Speicher zugewiesen (lang)
'+' Operator 747 320,504
String.format 16484 373,312
StringBuilder 769 57,344

Wir können sehen, dass String '+' und StringBuilder zeitlich praktisch identisch sind, aber StringBuilder ist wesentlich effizienter in der Speichernutzung. Dies ist sehr wichtig, wenn wir viele Protokollaufrufe (oder andere Anweisungen mit Zeichenfolgen) in einem ausreichend kurzen Zeitintervall haben, damit der Garbage Collector die vielen Zeichenfolgeninstanzen, die sich aus dem Operator '+' ergeben, nicht bereinigen kann.

Und eine Notiz, übrigens, vergessen Sie nicht, die Protokollierungsstufe zu überprüfen, bevor Sie die Nachricht erstellen.

Schlussfolgerungen:

  1. Ich werde weiterhin StringBuilder verwenden.
  2. Ich habe zu viel Zeit oder zu wenig Leben.
235
Itamar

Alle hier vorgestellten Benchmarks haben einige Fehler , daher sind die Ergebnisse nicht zuverlässig.

Ich war überrascht, dass niemand JMH für das Benchmarking verwendete, also tat ich es.

Ergebnisse:

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

Einheiten sind Operationen pro Sekunde, je mehr desto besser. Benchmark-Quellcode . OpenJDK IcedTea 2.5.4 Java Virtual Machine wurde verwendet.

Alter Stil (mit +) ist also viel schneller.

25

Ihr alter hässlicher Stil wird von JAVAC 1.6 automatisch kompiliert als:

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

Es gibt also absolut keinen Unterschied zu einem StringBuilder.

String.format ist viel schwerer, da es einen neuen Formatierer erstellt, Ihre Eingabeformatzeichenfolge analysiert, einen StringBuilder erstellt, alles daran anfügt und toString () aufruft.

21
Raphaël

Javas String.format funktioniert so:

  1. es analysiert die Formatzeichenfolge und zerfällt in eine Liste von Formatabschnitten
  2. es iteriert die Format-Chunks und rendert sie in einen StringBuilder, der im Grunde genommen ein Array ist, dessen Größe sich nach Bedarf ändert, indem es in ein neues Array kopiert. Dies ist erforderlich, da wir noch nicht wissen, wie groß die Zuweisung des endgültigen Strings ist
  3. StringBuilder.toString () kopiert seinen internen Puffer in einen neuen String

wenn das endgültige Ziel für diese Daten ein Stream ist (z. B. Rendern einer Webseite oder Schreiben in eine Datei), können Sie die Format-Chunks direkt in Ihrem Stream zusammenstellen:

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

Ich spekuliere, dass der Optimierer die Format-String-Verarbeitung wegoptimieren wird. Wenn ja, bleibt Ihnen die entsprechende amortisierte Leistung, um Ihr String.format manuell in einen StringBuilder zu entrollen.

12
Dustin Getz

Um die erste Antwort oben zu erweitern/zu korrigieren, ist es eigentlich keine Übersetzung, bei der String.format helfen würde.
Mit String.format können Sie ein Datum/eine Uhrzeit (oder ein numerisches Format usw.) drucken, bei denen es Unterschiede in der Lokalisierung (l10n) gibt (dh einige Länder geben den 04. Februar 2009 aus, andere den 04. Februar 2009) drucken Feb042009).
Bei der Übersetzung geht es nur darum, externe Zeichenfolgen (wie Fehlermeldungen und was nicht) in ein Eigenschaftenpaket zu verschieben, damit Sie mit ResourceBundle und MessageFormat das richtige Paket für die richtige Sprache verwenden können.

Wenn man sich das oben Genannte ansieht, würde ich sagen, dass es in Bezug auf die Leistung von String.format vs. Wenn Sie lieber Aufrufe von .format als Verkettung betrachten, sollten Sie dies unbedingt tun.
Schließlich wird Code viel mehr gelesen als geschrieben.

8
dw.mackie

In Ihrem Beispiel ist die probalby-Leistung nicht allzu unterschiedlich, es sind jedoch andere Aspekte zu berücksichtigen: die Speicherfragmentierung. Selbst bei verketteten Operationen wird eine neue Zeichenfolge erstellt, auch wenn diese temporär ist (es dauert einige Zeit, sie zu analysieren, und es ist mehr Arbeit). String.format () ist nur lesbarer und erfordert weniger Fragmentierung.

Wenn Sie häufig ein bestimmtes Format verwenden, vergessen Sie nicht, dass Sie die Formatter () - Klasse direkt verwenden können (alles, was String.format () tut, ist die Instanz eines Formatter, die nur einmal verwendet wird).

Außerdem sollten Sie Folgendes beachten: Achten Sie auf die Verwendung von substring (). Beispielsweise:

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

Diese große Zeichenfolge befindet sich immer noch im Speicher, da Java substrings) genau so funktioniert. Eine bessere Version ist:

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

oder

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

Die zweite Form ist wahrscheinlich nützlicher, wenn Sie andere Dinge gleichzeitig erledigen.

6
cletus

Im Allgemeinen sollten Sie String.Format verwenden, da es relativ schnell ist und die Globalisierung unterstützt (vorausgesetzt, Sie versuchen tatsächlich, etwas zu schreiben, das vom Benutzer gelesen wird). Dies erleichtert auch die Globalisierung, wenn Sie versuchen, eine Zeichenfolge gegenüber 3 oder mehr pro Anweisung zu übersetzen (insbesondere für Sprachen mit drastisch unterschiedlichen grammatikalischen Strukturen).

Wenn Sie nie vorhaben, etwas zu übersetzen, verlassen Sie sich entweder auf Javas eingebaute Konvertierung von + -Operatoren in StringBuilder. Oder benutze explizit Javas StringBuilder.

5
Orion Adrian

Eine andere Perspektive nur aus Sicht der Protokollierung.

Ich sehe eine Menge Diskussionen im Zusammenhang mit dem Einloggen in diesen Thread, also dachte ich darüber nach, meine Erfahrung als Antwort hinzuzufügen. Vielleicht findet es jemand nützlich.

Ich denke, die Motivation für die Protokollierung mit dem Formatierer liegt in der Vermeidung der Verkettung von Zeichenfolgen. Grundsätzlich möchten Sie keinen Overhead für string concat haben, wenn Sie es nicht protokollieren möchten.

Sie müssen nicht wirklich concat/formatieren, es sei denn, Sie möchten sich anmelden. Sagen wir, wenn ich eine Methode wie diese definiere

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

Bei diesem Ansatz wird der cancat/formatierer überhaupt nicht aufgerufen, wenn er eine Debug-Nachricht und debugOn = false ist

Es ist jedoch immer noch besser, hier StringBuilder anstelle von Formatierer zu verwenden. Die Hauptmotivation ist, all dies zu vermeiden.

Gleichzeitig mag ich es nicht, "if" -Blöcke für jede Protokollierungsanweisung hinzuzufügen

  • Dies beeinträchtigt die Lesbarkeit
  • Reduziert die Abdeckung meiner Unit-Tests - das ist verwirrend, wenn Sie sicherstellen möchten, dass jede Leitung getestet wird.

Daher bevorzuge ich es, eine Protokollierungsdienstprogrammklasse mit Methoden wie oben zu erstellen und sie überall zu verwenden, ohne mir Gedanken über Leistungseinbußen und andere damit zusammenhängende Probleme zu machen.

3

Ich habe gerade den Test von hhafez so geändert, dass er StringBuilder enthält. StringBuilder ist unter XP 33-mal schneller als String.format mit dem jdk 1.6.0_10-Client. Mit der Option -server wird der Faktor auf 20 gesenkt.

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

Obwohl dies drastisch klingen mag, halte ich es nur in seltenen Fällen für relevant, da die absoluten Zahlen ziemlich niedrig sind: 4 s für 1 Million einfache String.format-Aufrufe sind irgendwie in Ordnung - solange ich sie für die Protokollierung oder die Protokollierung verwende mögen.

Aktualisieren: Wie von sjbotha in den Kommentaren hervorgehoben, ist der StringBuilder-Test ungültig, da eine letzte .toString() fehlt.

Der korrekte Beschleunigungsfaktor von String.format(.) bis StringBuilder ist 23 auf meinem Computer (16 mit dem Schalter -server).

2
the.duckman

Hier ist eine modifizierte Version von hhafez entry. Es enthält eine String Builder-Option.

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

}

}

Zeit danach für Schleife 391 Zeit danach für Schleife 4163 Zeit danach für Schleife 227

1
ANON

Die Antwort darauf hängt stark davon ab, wie Ihr spezifischer Java) - Compiler den von ihm generierten Bytecode optimiert. Strings sind unveränderlich und theoretisch kann jede "+" - Operation eine neue erstellen. Aber Ihr Compiler Mit ziemlicher Sicherheit werden Zwischenschritte beim Erstellen langer Zeichenfolgen entfernt. Es ist durchaus möglich, dass beide obigen Codezeilen den exakt gleichen Bytecode generieren.

Die einzige Möglichkeit, dies zu erfahren, besteht darin, den Code in Ihrer aktuellen Umgebung iterativ zu testen. Schreiben Sie eine QD-App, die Zeichenfolgen in beide Richtungen iterativ verkettet, und sehen Sie, wie das Zeitlimit überschritten wird.

0

Erwägen Sie die Verwendung von "hello".concat( "world!" ) für eine geringe Anzahl von Zeichenfolgen in der Verkettung. Es könnte für die Leistung sogar noch besser sein als andere Ansätze.

Wenn Sie mehr als 3 Zeichenfolgen haben, sollten Sie StringBuilder oder nur String verwenden, je nachdem, welchen Compiler Sie verwenden.

0
Sasa