String Performance Hinweise

Hinweise zur Saitenleistung

**

1. Einführung

In diesem Tutorial werdenwe’re going to focus on the performance aspect of the Java String API.

Wir werden uns mit den Erstellungs-, Konvertierungs- und Änderungsvorgängen vonStringbefassen, um die verfügbaren Optionen zu analysieren und ihre Effizienz zu vergleichen.

Die Vorschläge, die wir machen werden, passen nicht unbedingt zu jeder Anwendung. Aber natürlich werden wir zeigen, wie Sie bei der Leistung gewinnen können, wenn die Laufzeit der Anwendung kritisch ist.

2. Erstellen eines neuen Strings

Wie Sie wissen, sind Strings in Java unveränderlich. Jedes Mal, wenn wir einString-Objekt erstellen oder verketten, erstellt Java ein neuesString –. Dies kann besonders kostspielig sein, wenn es in einer Schleife ausgeführt wird.

2.1. Konstruktor verwenden

In den meisten Fällenwe should avoid creating Strings using the constructor unless we know what are we doing.

Erstellen wir zuerst einnewString -Objekt innerhalb der Schleife, indem wir den Konstruktornew String()und dann den Operator=verwenden.

Zum Schreiben unseres Benchmarks verwenden wir das ToolJMH (Java Microbenchmark Harness).

Unsere Konfiguration:

@BenchmarkMode(Mode.SingleShotTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Measurement(batchSize = 10000, iterations = 10)
@Warmup(batchSize = 10000, iterations = 10)
public class StringPerformance {
}

Hier verwenden wir denSingeShotTime-Modus, in dem die Methode nur einmal ausgeführt wird. Da wir die Leistung derString-Operationen innerhalb der Schleife messen möchten, steht dafür eine@Measurement-Notation zur Verfügung.

Wichtig zu wissen, dassbenchmarking loops directly in our tests may skew the results because of various optimizations applied by JVM.

Also berechnen wir nur die Einzeloperation und überlassen JMH das Looping. Kurz gesagt, JMH führt die Iterationen unter Verwendung des ParametersbatchSize durch.

Fügen wir nun den ersten Mikro-Benchmark hinzu:

@Benchmark
public String benchmarkStringConstructor() {
    return new String("example");
}

@Benchmark
public String benchmarkStringLiteral() {
    return "example";
}

Im ersten Test wird in jeder Iteration ein neues Objekt erstellt. Im zweiten Test wird das Objekt nur einmal erstellt. Für die verbleibenden Iterationen wird dasselbe Objekt aus dem konstanten Pool vonString’szurückgegeben.

Lassen Sie uns die Tests mit der Anzahl der Schleifeniterationen= 1,000,000 ausführen und die Ergebnisse sehen:

Benchmark                   Mode  Cnt  Score    Error     Units
benchmarkStringConstructor  ss     10  16.089 ± 3.355     ms/op
benchmarkStringLiteral      ss     10  9.523  ± 3.331     ms/op

Aus denScore-Werten können wir deutlich erkennen, dass der Unterschied signifikant ist.

2.2. + Betreiber

Schauen wir uns das Verkettungsbeispiel von dynamicStringan:

@State(Scope.Thread)
public static class StringPerformanceHints {
    String result = "";
    String example = "example";
}

@Benchmark
public String benchmarkStringDynamicConcat() {
    return result + example;
}

In unseren Ergebnissen möchten wir die durchschnittliche Ausführungszeit sehen. Das Format der Ausgabenummer ist auf Millisekunden eingestellt:

Benchmark                       1000     10,000
benchmarkStringDynamicConcat    47.331   4370.411

Lassen Sie uns nun die Ergebnisse analysieren. Wie wir sehen, dauert das Hinzufügen von1000 Elementen zustate.result47.331 Millisekunden. Wenn die Anzahl der Iterationen um das Zehnfache erhöht wird, steigt die Laufzeit auf4370.441Millisekunden.

Zusammenfassend wächst die Ausführungszeit quadratisch. Daher beträgt die Komplexität der dynamischen Verkettung in einer Schleife von n IterationenO(n^2).

2.3. String.concat()

Eine weitere Möglichkeit,Strings zu verketten, ist die Verwendung der Methodeconcat():

@Benchmark
public String benchmarkStringConcat() {
    return result.concat(example);
}

Die Ausgabezeiteinheit ist eine Millisekunde, die Anzahl der Iterationen beträgt 100.000. Die Ergebnistabelle sieht folgendermaßen aus:

Benchmark              Mode  Cnt  Score     Error     Units
benchmarkStringConcat    ss   10  3403.146 ± 852.520  ms/op

2.4. String.format()

Eine andere Möglichkeit, Zeichenfolgen zu erstellen, ist die Verwendung der MethodeString.format(). Under the hood, it uses regular expressions to parse the input.

Schreiben wir den JMH-Testfall:

String formatString = "hello %s, nice to meet you";

@Benchmark
public String benchmarkStringFormat_s() {
    return String.format(formatString, example);
}

Danach führen wir es aus und sehen die Ergebnisse:

Number of Iterations      10,000   100,000   1,000,000
benchmarkStringFormat_s   17.181   140.456   1636.279    ms/op

Obwohl der Code mitString.format() sauberer und lesbarer aussieht, gewinnen wir hier in Bezug auf die Leistung nicht.

2.5. StringBuilder undStringBuffer

Wir haben bereits einwrite-up, dasStringBuffer undStringBuilder erklärt. Daher werden hier nur zusätzliche Informationen zu ihrer Leistung angezeigt. StringBuilder enthält ein Array mit veränderbarer Größe und einen Index, der die Position der zuletzt im Array verwendeten Zelle angibt. Wenn das Array voll ist, wird es um das Doppelte vergrößert und kopiert alle Zeichen in das neue Array.

Berücksichtigt man, dass die Größenänderung nicht sehr häufig auftritt,we can consider each append() operation as O(1) constant time. In Anbetracht dessen weist der gesamte Prozess eineO(n) -Skomplexität auf.

Nach dem Ändern und Ausführen des dynamischen Verkettungstests fürStringBuffer undStringBuilder, erhalten Sie Folgendes:

Benchmark               Mode  Cnt  Score   Error  Units
benchmarkStringBuffer   ss    10  1.409  ± 1.665  ms/op
benchmarkStringBuilder  ss    10  1.200  ± 0.648  ms/op

Obwohl der Unterschied in der Punktzahl nicht groß ist, können wirthat StringBuilder works faster feststellen.

Glücklicherweise benötigen wir in einfachen Fällen keineStringBuilder, um einString mit einem anderen zu verbinden. Manchmalstatic concatenation with + can actually replace StringBuilder. Under the hood, the latest Java compilers will call the StringBuilder.append() to concatenate strings.

Dies bedeutet einen deutlichen Leistungsgewinn.

3. Dienstprogrammbetrieb

3.1. StringUtils.replace() vsString.replace()

Interessant zu wissen, dassApache Commons version for replacing the String does way better than the String’s own replace() method. Die Antwort auf diesen Unterschied liegt in ihrer Umsetzung. String.replace() verwendet ein Regex-Muster, um mitString. übereinzustimmen

Im Gegensatz dazu verwendetStringUtils.replace() häufigindexOf(), was schneller ist.

Jetzt ist es Zeit für die Benchmark-Tests:

@Benchmark
public String benchmarkStringReplace() {
    return longString.replace("average", " average !!!");
}

@Benchmark
public String benchmarkStringUtilsReplace() {
    return StringUtils.replace(longString, "average", " average !!!");
}

Wenn SiebatchSize auf 100.000 setzen, präsentieren wir die Ergebnisse:

Benchmark                     Mode  Cnt  Score   Error   Units
benchmarkStringReplace         ss   10   6.233  ± 2.922  ms/op
benchmarkStringUtilsReplace    ss   10   5.355  ± 2.497  ms/op

Obwohl der Unterschied zwischen den Zahlen nicht zu groß ist, hatStringUtils.replace() eine bessere Punktzahl. Natürlich können die Zahlen und der Abstand zwischen ihnen abhängig von Parametern wie Iterationsanzahl, Stringlänge und sogar der JDK-Version variieren.

Mit den neuesten Versionen von JDK 9+ (unsere Tests laufen auf JDK 10) erzielen beide Implementierungen ziemlich gleiche Ergebnisse. Lassen Sie uns nun die JDK-Version auf 8 und die Tests erneut herabstufen:

Benchmark                     Mode  Cnt   Score    Error     Units
benchmarkStringReplace         ss   10    48.061   ± 17.157  ms/op
benchmarkStringUtilsReplace    ss   10    14.478   ±  5.752  ms/op

Der Leistungsunterschied ist jetzt enorm und bestätigt die Theorie, die wir am Anfang besprochen haben.

3.2. split()

Bevor wir beginnen, ist es hilfreich, die in Java verfügbaren Zeichenfolgensplitting methodszu überprüfen.

Wenn eine Zeichenfolge mit dem Trennzeichen geteilt werden muss, ist die erste Funktion, die uns normalerweise in den Sinn kommt,String.split(regex). Es bringt jedoch einige schwerwiegende Leistungsprobleme mit sich, da es ein reguläres Argument akzeptiert. Alternativ können wir die KlasseStringTokenizerverwenden, um die Zeichenfolge in Token aufzuteilen.

Eine weitere Option ist dieSplitter-API von Guava. Schließlich ist das gute alteindexOf() auch verfügbar, um die Leistung unserer Anwendung zu steigern, wenn wir die Funktionalität regulärer Ausdrücke nicht benötigen.

Jetzt ist es Zeit, die Benchmark-Tests für die OptionString.split()zu schreiben:

String emptyString = " ";

@Benchmark
public String [] benchmarkStringSplit() {
    return longString.split(emptyString);
}

Pattern.split():

@Benchmark
public String [] benchmarkStringSplitPattern() {
    return spacePattern.split(longString, 0);
}

StringTokenizer:

List stringTokenizer = new ArrayList<>();

@Benchmark
public List benchmarkStringTokenizer() {
    StringTokenizer st = new StringTokenizer(longString);
    while (st.hasMoreTokens()) {
        stringTokenizer.add(st.nextToken());
    }
    return stringTokenizer;
}

String.indexOf():

List stringSplit = new ArrayList<>();

@Benchmark
public List benchmarkStringIndexOf() {
    int pos = 0, end;
    while ((end = longString.indexOf(' ', pos)) >= 0) {
        stringSplit.add(longString.substring(pos, end));
        pos = end + 1;
    }
    return stringSplit;
}

GuavasSplitter:

@Benchmark
public List benchmarkGuavaSplitter() {
    return Splitter.on(" ").trimResults()
      .omitEmptyStrings()
      .splitToList(longString);
}

Schließlich führen wir die Ergebnisse fürbatchSize = 100,000 aus und vergleichen sie:

Benchmark                     Mode  Cnt    Score    Error    Units
benchmarkGuavaSplitter         ss   10    4.008  ± 1.836     ms/op
benchmarkStringIndexOf         ss   10    1.144  ± 0.322     ms/op
benchmarkStringSplit           ss   10    1.983  ± 1.075     ms/op
benchmarkStringSplitPattern    ss   10    14.891  ± 5.678    ms/op
benchmarkStringTokenizer       ss   10    2.277  ± 0.448     ms/op

Wie wir sehen, hat die schlechteste Leistung diebenchmarkStringSplitPattern-Methode, bei der wir diePattern-Klasse verwenden. Infolgedessen können wir feststellen, dass die Verwendung einer Regex-Klasse mit der Methodesplit()mehrmals zu Leistungseinbußen führen kann.

Ebensowe notice that the fastest results are providing examples with the use of indexOf() and split().

3.3. Umrechnung inString

In diesem Abschnitt werden die Laufzeitwerte der Zeichenfolgenkonvertierung gemessen. Um genauer zu sein, werden wir die Verkettungsmethode vonInteger.toString()untersuchen:

int sampleNumber = 100;

@Benchmark
public String benchmarkIntegerToString() {
    return Integer.toString(sampleNumber);
}

String.valueOf():

@Benchmark
public String benchmarkStringValueOf() {
    return String.valueOf(sampleNumber);
}

[some integer value] + “”:

@Benchmark
public String benchmarkStringConvertPlus() {
    return sampleNumber + "";
}

String.format():

String formatDigit = "%d";

@Benchmark
public String benchmarkStringFormat_d() {
    return String.format(formatDigit, sampleNumber);
}

Nach dem Ausführen der Tests wird die Ausgabe fürbatchSize = 10,000 angezeigt:

Benchmark                     Mode  Cnt   Score    Error  Units
benchmarkIntegerToString      ss   10   0.953 ±  0.707  ms/op
benchmarkStringConvertPlus    ss   10   1.464 ±  1.670  ms/op
benchmarkStringFormat_d       ss   10  15.656 ±  8.896  ms/op
benchmarkStringValueOf        ss   10   2.847 ± 11.153  ms/op

Nach der Analyse der Ergebnisse sehen wir, dassthe test for Integer.toString() has the best score of 0.953 milliseconds. Im Gegensatz dazu weist eine Konvertierung mitString.format(“%d”) die schlechteste Leistung auf.

Dies ist logisch, da das Parsen des FormatsString eine teure Operation ist.

3.4. Strings vergleichen

Lassen Sie uns verschiedene Arten des Vergleichs vonStrings. bewerten. Die Anzahl der Iterationen beträgt100,000.

Hier sind unsere Benchmark-Tests für den Betrieb vonString.equals():

@Benchmark
public boolean benchmarkStringEquals() {
    return longString.equals(example);
}

String.equalsIgnoreCase():

@Benchmark
public boolean benchmarkStringEqualsIgnoreCase() {
    return longString.equalsIgnoreCase(example);
}

String.matches():

@Benchmark
public boolean benchmarkStringMatches() {
    return longString.matches(example);
}

String.compareTo():

@Benchmark
public int benchmarkStringCompareTo() {
    return longString.compareTo(example);
}

Danach führen wir die Tests durch und zeigen die Ergebnisse an:

Benchmark                         Mode  Cnt    Score    Error  Units
benchmarkStringCompareTo           ss   10    2.561 ±  0.899   ms/op
benchmarkStringEquals              ss   10    1.712 ±  0.839   ms/op
benchmarkStringEqualsIgnoreCase    ss   10    2.081 ±  1.221   ms/op
benchmarkStringMatches             ss   10    118.364 ± 43.203 ms/op

Wie immer sprechen die Zahlen für sich. Dasmatches() benötigt die längste Zeit, da es den regulären Ausdruck verwendet, um die Gleichheit zu vergleichen.

Im Gegensatz dazu istthe equals() and equalsIgnoreCase() are the best choices.

3.5. String.matches() vsPrecompiled Pattern

Schauen wir uns nun die SpatternString.matches() undMatcher.matches() eparat an. Der erste nimmt einen regulären Ausdruck als Argument und kompiliert ihn, bevor er ausgeführt wird.

Jedes Mal, wenn wirString.matches() aufrufen, werden diePattern: kompiliert

@Benchmark
public boolean benchmarkStringMatches() {
    return longString.matches(example);
}

Die zweite Methode verwendet dasPattern-Objekt erneut:

Pattern longPattern = Pattern.compile(longString);

@Benchmark
public boolean benchmarkPrecompiledMatches() {
    return longPattern.matcher(example).matches();
}

Und jetzt die Ergebnisse:

Benchmark                      Mode  Cnt    Score    Error   Units
benchmarkPrecompiledMatches    ss   10    29.594  ± 12.784   ms/op
benchmarkStringMatches         ss   10    106.821 ± 46.963   ms/op

Wie wir sehen, funktioniert das Matching mit vorkompiliertem regulären Ausdruck ungefähr dreimal schneller.

3.6. Länge überprüfen

Vergleichen wir abschließend dieString.isEmpty()-Methode:

@Benchmark
public boolean benchmarkStringIsEmpty() {
    return longString.isEmpty();
}

und dieString.length() Methode:

@Benchmark
public boolean benchmarkStringLengthZero() {
    return emptyString.length() == 0;
}

Zuerst rufen wir sie überlongString = “Hello example, I am a bit longer than other Strings in average” String. auf.batchSize ist10,000:

Benchmark                  Mode  Cnt  Score   Error  Units
benchmarkStringIsEmpty       ss   10  0.295 ± 0.277  ms/op
benchmarkStringLengthZero    ss   10  0.472 ± 0.840  ms/op

Setzen Sie anschließend die leere Zeichenfolge vonlongString = “”und führen Sie die Tests erneut aus:

Benchmark                  Mode  Cnt  Score   Error  Units
benchmarkStringIsEmpty       ss   10  0.245 ± 0.362  ms/op
benchmarkStringLengthZero    ss   10  0.351 ± 0.473  ms/op

Wie wir bemerken, habenbenchmarkStringLengthZero()- undbenchmarkStringIsEmpty() m-Methoden in beiden Fällen ungefähr die gleiche Punktzahl. Aufrufen vonisEmpty() works faster than checking if the string’s length is zero.

4. String-Deduplizierung

Seit JDK 8 ist die String-Deduplizierungsfunktion verfügbar, um den Speicherverbrauch zu verringern. Einfach ausgedrückt,this tool is looking for the strings with the same or duplicate contents to store one copy of each distinct string value into the String pool.

Derzeit gibt es zwei Möglichkeiten, mitString Duplikaten umzugehen:

  • Verwenden Sie dieString.intern() manuell

  • Aktivieren der String-Deduplizierung

Schauen wir uns jede Option genauer an.

4.1. String.intern()

Bevor Sie fortfahren, sollten Sie in unserenwrite-up Informationen zum manuellen Internieren lesen. With String.intern() we can manually set the reference of the String object inside of the global String pool.

Anschließend kann JVM die Referenz bei Bedarf zurückgeben. Unter dem Gesichtspunkt der Leistung kann unsere Anwendung enorm davon profitieren, wenn die Zeichenfolgenreferenzen aus dem konstanten Pool wiederverwendet werden.

Wichtig zu wissen, dassJVM String pool isn’t local for the thread. Each String that we add to the pool, is available to other threads as well.

Es gibt jedoch auch gravierende Nachteile:

  • Um unsere Anwendung ordnungsgemäß zu verwalten, müssen wir möglicherweise einen JVM-Parameter von-XX:StringTableSize festlegen, um die Poolgröße zu erhöhen. JVM muss neu gestartet werden, um die Poolgröße zu erweitern

  • calling String.intern() manually is time-consuming. Es wächst in einem linearen Zeitalgorithmus mit der Komplexität vonO(n)

  • zusätzlichfrequent calls on long String objects may cause memory problems

Führen Sie einen Benchmark-Test durch, um einige nachgewiesene Zahlen zu erhalten:

@Benchmark
public String benchmarkStringIntern() {
    return example.intern();
}

Außerdem werden die Ausgabewerte in Millisekunden angegeben:

Benchmark               1000   10,000  100,000  1,000,000
benchmarkStringIntern   0.433  2.243   19.996   204.373

Die Spaltenüberschriften hier stellen unterschiedlicheiterations-Zählungen von1000 bis1,000,000 dar. Für jede Iterationsnummer haben wir die Testleistungsbewertung. Wie wir bemerken, steigt der Score zusätzlich zur Anzahl der Iterationen dramatisch an.

4.2. Deduplizierung automatisch aktivieren

Zunächstthis option is a part of the G1 garbage collector. Standardmäßig ist diese Funktion deaktiviert. Also müssen wir es mit dem folgenden Befehl aktivieren:

 -XX:+UseG1GC -XX:+UseStringDeduplication

Es ist wichtig zu beachten, dassenabling this option doesn’t guarantee that String deduplication will happen. Außerdem werden jungeStrings.nicht verarbeitet. Um das Mindestalter für die Verarbeitung zu verwalten, ist die JVM-OptionStrings, XX:StringDeduplicationAgeThreshold=3verfügbar. Hier ist3 der Standardparameter.

5. Zusammenfassung

In diesem Tutorial versuchen wir, einige Hinweise zu geben, wie Sie Zeichenfolgen in unserem täglichen Codierungsleben effizienter verwenden können.

Als Ergebnis istwe can highlight some suggestions in order to boost our application performance:

  • when concatenating strings, the StringBuilder is the most convenient option, die mir in den Sinn kommen. Mit den kleinen Zeichenfolgen hat die Operationjedoch fast die gleiche Leistung. Unter der Haube kann der Java-Compiler dieStringBuilder -Skala verwenden, um die Anzahl der Zeichenfolgenobjekte zu reduzieren

  • Um den Wert in die Zeichenfolge umzuwandeln, arbeiten[some type].toString() (Integer.toString() zum Beispiel) schneller alsString.valueOf(). Da dieser Unterschied nicht signifikant ist, können wirString.valueOf() frei verwenden, um keine Abhängigkeit vom Eingabewerttyp zu haben

  • Wenn es um den Vergleich von Zeichenfolgen geht, geht bisher nichts über dieString.equals()

  • Die Deduplizierung vonStringverbessert die Leistung in großen Multithread-Anwendungen. Eine übermäßige Verwendung vonString.intern() kann jedoch zu schwerwiegenden Speicherverlusten führen und die Anwendung verlangsamen

  • for splitting the strings we should use indexOf() to win in performance. In einigen unkritischen Fällen kann jedoch die Funktion vonString.split()gut passen

  • MitPattern.match() verbessert die Zeichenfolge die Leistung erheblich

  • String.isEmpty() ist schneller als String.length() ==0

Außerdemkeep in mind that the numbers we present here are just JMH benchmark results - Sie sollten also immer im Rahmen Ihres eigenen Systems und Ihrer Laufzeit testen, um die Auswirkungen dieser Art von Optimierungen zu ermitteln.

Schließlich kann wie immer der während der Diskussion verwendete Codeover on GitHub gefunden werden.

**