int in String umwandeln in Java - die schnellste Methode - Feature-Bild

Java: int in String umwandeln – der schnellste Weg

In diesem Artikel zeige ich euch, wie ihr in Java am schnellsten ein int in einen String umwandelt. Die Antwort wird für einige sicher überraschend sein. Ich stelle euch vier Varianten vor. Deren Geschwindkeit messe und vergleiche ich mit Hilfe von JMH-Microbenchmarks. Die Messergebnisse werde ich mit Hilfe des Java-Quellcodes und auch des erzeugten Bytecodes analysieren. Falls du die Details überspringen möchtest, kannst Du über diesen Link direkt zum Ergebnis runterscrollen.

Varianten für die int-to-String-Konvertierung

Folgende vier Optionen gibt es (abgesehen von absichtlich komplizierter konstruierten Varianten):

  • Option 1: Integer.toString(i)
  • Option 2: String.valueOf(i)
  • Option 3: String.format("%d", i)
  • Option 4: "" + i

Quiz

Was denkst Du? Welche Option ist die schnellste?

Im Folgenden werde ich zunächst ausführliche Benchmarks durchführen und danach die Ergebnisse anhand des Java-Quellcodes und des erzeugten Bytecodes interpretieren.

Performance-Messungen der int-zu-String-Umwandlung

Um zu ermitteln, welche der Optionen die schnellste ist, habe ich Benchmarks mit dem Java Microbenchmark Harness – kurz: JMH – durchgeführt.

Der JMH ist ein Framework, der Benchmark-Tests für kurze Code-Ausschnitte ermöglicht und aussagekräftige Messwerte im Milli-, Mikro- und Nanosekundenbereich liefert. Dazu werden die Tests hunderttausendfach wiederholt, und der eigentliche Messvorgang wird erst nach einer Aufwärmphase gestartet, um dem Just-in-Time-Compiler ausreichend Vorlaufzeit für die Code-Optimierung zu gewähren.

Ein sehr gutes Einsteiger-Tutorial findet ihr auf tutorials.jenkov.com.

IntelliJ kommt standardmäßig mit einem JMH-Plugin, so dass ihr die Benchmark-Tests direkt in der IDE ausführen könnt.

Quellcode der Microbenchmarks

Im Folgenden findet ihr den kompletten Quellcode des int-to-String-Benchmarks. Ihr könnt den Code direkt in eure IDE kopieren oder ihn als Maven-Ptojekt aus meinem GitLab-Repository klonen. Wenn ihr selbst ein Projekt anlegt, müsst ihr die folgenden zwei Dependencies hinzufügen:

package eu.happycoders.int2string;

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;

import java.util.concurrent.ThreadLocalRandom;

public class IntToStringBenchmark {

    @State(Scope.Thread)
    public static class MyState {
        public int i;

        @Setup(Level.Invocation)
        public void doSetup() {
            // always 7-digits, so that the String always has the same length
            i = 1_000_000 + ThreadLocalRandom.current().nextInt(9_000_000);
        }
    }

    @Benchmark
    public void option1(MyState state, Blackhole blackhole) {
        String s = Integer.toString(state.i);
        blackhole.consume(s);
    }

    @Benchmark
    public void option2(MyState state, Blackhole blackhole) {
        String s = String.valueOf(state.i);
        blackhole.consume(s);
    }

    @Benchmark
    public void option3(MyState state, Blackhole blackhole) {
        String s = String.format("%d", state.i);
        blackhole.consume(s);
    }

    @Benchmark
    public void option4(MyState state, Blackhole blackhole) {
        String s = "" + state.i;
        blackhole.consume(s);
    }

}

Ein paar Anmerkungen zum Quellcode:

  • Die int-Variable i belege ich mit einer Zufallszahl, damit sie nicht durch eine Konstante ersetzt und die komplette String-Umwandlung wegoptimiert wird.
  • Die Erzeugung der Zufallszahl wird in die setup()-Methode eines sogenannten „State“ ausgelagert, so dass die Ausführungszeit dafür nicht mitgemessen wird.
  • Durch die Annotation @Setup(Level.Invocation) wird die setup()-Methode vor jedem Aufruf der Testmethode ausgeführt; somit erhält jeder Aufruf eine neue Zufallszahl.
  • Das Konvertierungsergebnis wird jeweils dem Blackhole übergeben – wiederum damit der Compiler die Konvertierung nicht wegoptimiert.

Microbenchmark-Ergebnisse

Im Folgenden findet ihr die Messergebnisse auf meinem Dell XPS 15 9570 mit einem Intel Core i7-8750H. Detaillierte Messergebnisse (inkl. aller Einzeltests, Minima, Maxima und Standardabweichungen) findet ihr im results/-Verzeichnis meines GitLab-Repositorys.

Messergebnisse int-to-String mit Java 7

MethodeOperationen pro SekundeKonfidenz​​intervall (99,9 %)
Integer.​​toString(i)20.365.94720.276.015 – 20.455.879
String.​​valueOf(i)20.318.31620.251.621 – 20.385.011
String.​​format("%d", i)2.107.3972.075.553 – 2.139.240
"" + i23.358.66823.178.506 – 23.538.831
Java 7 int-to-String Performance
Java 7 int-to-String Performance

Die ersten zwei Varianten können als gleich schnell angesehen werden, was zu erwarten ist, da String.valueOf(i) lediglich Integer.toString(i) aufruft und dieser Aufruf durch den HotSpot-Compiler wegoptimiert wird.

Die dritte Variante ist erwartungsgemäß langsamer, da hier der Format-String geparst werden muss.

Die vierte Variante ("" + i) ist unter Java 7 deutlich schneller (knapp 15 %) als die ersten zwei Varianten.

Messergebnisse int-to-String mit Java 8

MethodeOperationen pro SekundeKonfidenz​intervall (99,9 %)
Integer.​toString(i)20.939.91020.699.671 – 21.180.149
String.​valueOf(i)20.920.35920.737.898 – 21.102.821
String.​format("%d", i)2.284.0272.218.004 – 2.350.050
"" + i23.777.73823.651.239 – 23.904.237
Java 8 int-to-String Performance
Java 8 int-to-String Performance

Bei Java 8 zeigt sich ein sehr ähnliches Ergebnis wie bei Java 7, wobei alle Varianten zwischen 2 % und 8 % an Geschwindigkeit zugelegt haben.

Messergebnisse int-to-String mit Java 9

MethodeOperationen pro SekundeKonfidenz​​intervall (99,9 %)
Integer.​toString(i)28.025.70027.829.430 – 28.221.969
String.​​valueOf(i)27.732.47427.646.937 – 27.818.010
String.​​format("%d", i)2.718.3772.680.574 – 2.756.179
"" + i28.354.69028.151.883 – 28.557.497
Java 9 int-to-String Performance
Java 9 int-to-String Performance

Bei Java 9 wird es interessant: Alle Varianten haben deutlich (20 bis 30 %) an Durchsatz zugelegt. Allerdings ist der Vorsprung von Variante 4 ("" + i) auf etwa 2 % zurückgefallen. Um Messungenauigkeiten auszuschließen, habe ich den Test mehrfach wiederholt.

Messergebnisse int-to-String mit Java 11

Java 10 überspringe ich. Java 11 ist das aktuelle LTS (Long Term Support) Release. Java 9 war das erste Release nach dem neuen Release-Zyklus und kam dreieinhalb Jahre nach Java 8, daher habe ich es mit aufgenommen.

MethodeOperationen pro SekundeKonfidenz​intervall (99,9 %)
Integer.​toString(i)27.755.91427.537.830 – 27.973.998
String.​valueOf(i)27.836.73527.676.576 – 27.996.894
String.​format("%d", i)2.717.5512.602.165 – 2.832.937
"" + i28.237.06627.965.904 – 28.508.227
Java 11 int-to-String Performance
Java 11 int-to-String Performance

Von Java 9 zu Java 11 gab es keine nennenswerten Veränderungen mehr. Die minimalen Schwankungen führe ich auf Messungenauigkeiten zurück.

Messergebnisse int-to-String mit Java 13

Auch Java 12 überspringe ich und komme direkt zum aktuellen Release, Java 13.

MethodeOperationen pro SekundeKonfidenz​intervall (99,9 %)
Integer.​toString(i)27.664.97627.591.996 – 27.737.955
String.​valueOf(i)27.718.09627.646.080 – 27.790.112
String.​format("%d", i)1.800.3451.763.017 – 1.837.672
"" + i28.293.22828.156.241 – 28.430.215
Java 13 int-to-String Performance
Java 13 int-to-String Performance

Die Varianten eins, zwei und vier sind quasi unverändert, Variante vier ist nach wie vor der Spitzenreiter. Interessant wird es bei Variante drei (String.format("%d", i)): diese ist im Vergleich zu Java 11 knapp 34 % langsamer geworden.

Messergebnisse int-to-String mit Java 14

Der Vollständigkeit halber habe ich den Test mit dem letzten Early-Access-Build von Java 14 (ea+19) durchgeführt.

MethodeOperationen pro SekundeKonfidenz​intervall (99,9 %)
Integer.​toString(i)27.642.63027.484.253 – 27.801.007
String.​valueOf(i)27.571.93827.456.427 – 27.687.448
String.​format("%d", i)1.828.3821.780.958 – 1.875.807
"" + i28.226.17528.030.308 – 28.422.042
Java 14 int-to-String Performance
Java 14 int-to-String Performance

Hier sehen wir quasi das gleiche Ergebnis wie bei Java 13. "" + i ist noch immer der schnellste Weg. Der Vorsprung von etwa 2 % gegenüber den ersten zwei Varianten hat sich über die letzten vier gemessenen Java-Versionen bestätigt, so dass ich eine Messungenauigkeit ausschließe.

Messergebnis-Übersicht über alle Java Versionen

Hier seht ihr noch einmal alle Messergebnisse in einem Diagramm zusammengefasst:

Java int-to-String Performance
Java int-to-String Performance

Wir können zusammenfassend festhalten – unabhängig von der Java-Version:


… wobei der Vorsprung bis Java 8 mit knapp 15 % noch deutlich größer war als seit Java 9 mit etwa 2 %.

Erklärungen für die Performance-Unterschiede

Durch die Messungen sind folgende Fragen aufgekommen, die ich in diesem Abschnitt klären möchte:

  • Warum haben in Java 9 alle Varianten deutlich an Geschwindigkeit zugelegt?
  • Warum performt "" + i durchgehend am besten?
  • Warum ist String.format(i) in Java 13 so viel langsamer geworden?

Ich werde dazu den Java-Sourcecode sowie den generierten Bytecode analysieren.

Warum haben in Java 9 alle Varianten deutlich an Geschwindigkeit zugelegt?

Meine erste Vermutung war, dass die standardmäßig aktivierten „Compact Strings“ in Java 9 für die gesteigerte Performance verantwortlich sind. Allerdings ergab sich durch das Abschalten dieser (VM-Option „-XX:-CompactStrings“) keine relevante Änderung der Geschwindigkeit.

Mein zweiter Ansatz war den Java-Quellcode der Integer.toString(i)-Methode von Java 8 und Java 9 zu vergleichen. Während der Java 9-Code gut verständlich ist, sieht der Java 8-Code ziemlich kryptisch und optimiert aus. Um zu prüfen, ob es an dieser Code-Änderung liegt (und nicht an JVM-Optimierungen), habe ich den Java 8-Quellcode von Integer extrahiert, diesen unter Java 9 in eine Klasse Integer8 kopiert und mit dieser den Benchmark-Test wiederholt. Und tatsächlich: Die Integer8.toString(i)-Methode war auch unter Java 9 deutlich langsamer, allerdings auch etwas schneller als unter Java 8. Die Hauptursache für den Performancegewinn liegt also in Code-Verbesserungen, und hinzu kommen noch einige JVM-Optimierungen.

MethodeOperationen pro SekundeKonfidenz​intervall (99,9 %)
Java 8-Code unter Java 820.939.91020.699.671 – 21.180.149
Java 8-Code unter Java 921.737.98121.517.415 – 21.958.547
Java 9-Code unter Java 928.025.70027.829.430 – 28.221.969
Performance der Integer.toString()-Methode unter Java 8 und Java 9
Performance der Integer.toString()-Methode unter Java 8 und Java 9

Den zugehörigen Quellcode habe ich nicht in meinem GitLab-Repository hinterlegt, da ich mir nicht sicher bin, in wie weit ich Code aus dem JDK, oder auch nur Teile davon, veröffentlichen darf.

Die Varianten drei und vier habe ich an dieser Stelle nicht weiter untersucht. Ich gehe davon aus, dass auch dort die Algorithmen massiv verbessert wurden.

Warum performt "" + i durchgehend am besten?

Um zu ermitteln, warum "" + i am schnellsten ist, schauen wir uns zunächst einmal den erzeugten Bytecode an. Das machen wir wie folgt:

Wir erstellen eine Datei IntToStringFast.java mit folgendem Inhalt.

package eu.happycoders.int2string;

import java.util.Random;

public class IntToStringFast {
    public static void main(String[] args) {
        int i = new Random().nextInt();
        System.out.println("" + i);
    }
}

Wir kompilieren die Datei wie folgt:

javac IntToStringFast.java

Und lassen uns nun wie folgt den Bytecode anzeigen:

javap -c IntToStringFast.class

Variante "" + i unter Java 7 und Java 8

Unter Java 7 und Java 8 wird folgender Bytecode erzeugt (hier nur der relevante Auszug):

14: new           #6                  // class java/lang/StringBuilder
17: dup
18: invokespecial #7                  // Method java/lang/StringBuilder."":()V
21: ldc           #8                  // String
23: invokevirtual #9                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
26: iload_1
27: invokevirtual #10                 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
30: invokevirtual #11                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;

Dies entspricht folgendem Java-Code (interessanterweise wurde append("") nicht wegoptimiert – das wird hier wohl dem HotSpot-Compiler überlassen):

new StringBuilder().append("").append(i).toString();

Um diese Annahme zu bestätigen, führe ich den Benchmark-Test noch einmal mit diesem Code aus und komme zum selben Ergebnis wie für "" + i.

Wenn wir uns die Methode AbstractStringBuilder.append(int i) anschauen, stellen wir fest, dass diese sowohl unter Java 7 als auch unter Java 8 praktisch den gleichen Code enthält wie Integer.toString(int i). Der einzige Unterschied, den ich erkennen konnte, ist dass der StringBuilder intern zunächst ein char-Array der Länge 16 anlegt, welches dann in der toString()-Methode per System.arraycopy() in ein Array der tatsächlich benötigten Länge kopiert wird, während Integer.toString() von vornherein ein char-Array der final benötigten Länge erzeugt. Um zu prüfen, ob das einen Unterschied ausmacht, erstelle ich einen weiteren Test, in dem ich bei der Erzeugung des StringBuilders als Kapazität 7 mit übergebe (im Test werden ausschließlich 7-stellige Zufallszahlen erzeugt). Doch auch dies führt wieder zum selben Ergebnis.

Hier ein kurzer Zwischenstand meines aktuellen Tests:

Benchmark                                                        Mode  Cnt         Score   Error  Units
IntToStringBenchmarkStringBuilder.integerToString               thrpt    2  21168090,044          ops/s
IntToStringBenchmarkStringBuilder.stringBuilderCapacity7        thrpt    2  23968649,108          ops/s
IntToStringBenchmarkStringBuilder.stringBuilderCapacityDefault  thrpt    2  23769306,792          ops/s
IntToStringBenchmarkStringBuilder.stringPlus                    thrpt    2  23989334,180          ops/s

Alle StringBuilder-Variationen sind nahezu gleich schnell und, nach wie vor, deutlich schneller als Integer.toString(). Um der Ursache weiter auf den Grund zu gehen, kopiere ich den Quellcode sowohl von Integer als auch von StringBuilder und führe die Tests damit erneut aus. Ich erhalten folgendes Ergebnis (die Benchmarks mit der „8“ sind die mit dem kopierten Quellcode):

Benchmark                                                  Mode  Cnt         Score   Error  Units
IntToStringBenchmarkStringBuilderInline.integerToString   thrpt    2  20518228,534          ops/s
IntToStringBenchmarkStringBuilderInline.integer8ToString  thrpt    2  19681140,450          ops/s
IntToStringBenchmarkStringBuilderInline.stringBuilder     thrpt    2  23873235,183          ops/s
IntToStringBenchmarkStringBuilderInline.stringBuilder8    thrpt    2  19990576,858          ops/s 

Interessant: Im kopierten Quellcode sind Integer.toString(i) und "" + i beinahe gleich schnell – wie ich es nach Sichtung des Quellcodes eigentlich auch erwartet hätte. Beide kopierte Klassen sind jedoch langsamer als die Klassen aus dem JDK. Was kann das bedeuten?

Stimmen die kompilierten Klassen im JDK nicht mit dem mitgelieferten Quellcode überein? Um das zu prüfen, entferne ich die Datei src.zip aus dem JDK-Verzeichnis, so dass IntelliJ beim Klick auf eine Klasse nicht den mitgelieferten Quellcode anzeigt, sondern die Klasse dekompiliert. Die dekompilierte Klasse sieht exakt so aus wie der Quellcode (bis auf dass lokale Variablennamen generisch sind).

An dieser Stelle breche ich die Analyse für Java 7 und Java 8 vorerst ab. Man könnte sich jetzt noch mit den VM-Optionen -XX:+PrintCompilation -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining anzeigen lassen, wie HotSpot den Code im Detail optimiert, doch dafür fehlt mir die Zeit.

Variante "" + i seit Java 9

Seit Java 9 wird aus "" + i ein anderer Bytecode erzeugt – und zwar nur eine einzige Zeile:

15: invokedynamic #6,  0              // InvokeDynamic #0:makeConcatWithConstants:(I)Ljava/lang/String;

Die Methode makeConcatWithConstants() „ermöglicht die Erstellung von optimierten String-Verkettungsmethoden, die verwendet werden können, um eine bekannte Anzahl von Argumenten bekannter Typen effizient zu verketten“ (s. StringConcatFactory-JavaDoc). Ich habe mir den SourceCode von StringConcatFactory.makeConcatWithConstants() angeschaut. Letztendlich ruft diese – über ein MethodHandle – auch wieder StringBuilder.append(int) auf. Von daher ist auch hier nicht ohne weiteres erkennbar, warum diese Variante schneller ist als Integer.toString(). Erneut scheinen ausgefeilte HotSpot-Optimierungen am Werk zu sein.

Warum ist String.format(i) in Java 13 so langsam geworden?

Die Methode String.format() ruft sowohl in Java 11 als auch in Java 13 Formatter().format(format, args).toString() auf. Um zu prüfen, ob es an der int-zu-String-Umwandlung liegt oder am Formatter generell, führe ich einen Test mit dem Format-String „%s“ durch. Als Parameter übergebe ich wieder eine Zufallszahl, die ich schon im „State“ in einen String umwandle und als solchen an die Test-Methode übergebe (ihr findet auch diesen Test im GitLab-Repository).

Java-VersionOperationen pro SekundeKonfidenz​intervall (99,9 %)
Java 112.978.2412.377.067 – 3.579.416
Java 131.924.1831.624.398 – 2.223.968
Performance der String.format()-Methode unter Java 11 und Java 13
Performance der String.format()-Methode unter Java 11 und Java 13

Der Formatter ist also generell deutlich langsamer geworden, nicht nur bei der Umwandlung von Integern zu Strings. Um dies tiefergehend zu analysieren fehlt mir aber an dieser Stelle die Zeit. Dieser Artikel ist schon deutlich umfangreicher geworden als ursprünglich geplant.

Zusammenfassung und Ausblick

Ausführliche Benchmark-Tests haben gezeigt, dass über alle Java-Versionen hinweg "" + i der schnellste Weg ist, um ein Integer in einen String umzuwandeln. Lag der Vorsprung bei Java 7 und Java 8 noch bei beeindruckenden 15 %, ist er seit Java 9 auf etwa 2 % zurückgegangen, so dass es heute im Grunde genommen Geschmacksache ist, welche Variante ihr verwendet.

Leider ist es mir nicht gelungen den Grund dafür herauszufinden. Kennst du die Ursache? Oder kennst du noch andere performante Wege, um ints in Strings zu konvertieren? Dann freue ich mich über deinen Kommentar!

Im nächsten Artikel zeige ich euch, was in der entgegengesetzten Richtung, also beim Parsen von Strings in ints zu beachten ist.

Kommentar verfassen

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.