Wie wird die String-Verkettung in Java 9 implementiert?

111

Wie in JEP 280 geschrieben: Zeichenfolgenverkettung angeben :

Ändern Sie die StringBytecode-Sequenz für die statische Verkettung, die von generiert wird javac, um invokedynamicAufrufe von JDK-Bibliotheksfunktionen zu verwenden. Dies ermöglicht zukünftige Optimierungen der StringVerkettung, ohne dass weitere Änderungen am von ausgegebenen Bytecode erforderlich sind javac.

Hier möchte ich verstehen, wie invokedynamicAnrufe verwendet werden und wie sich die Bytecode-Verkettung von der unterscheidet invokedynamic.

Mohit Tyagi
quelle
11
Ich habe vor einiger Zeit darüber geschrieben - wenn das hilft, werde ich es zu einer Antwort zusammenfassen.
Nicolai
10
Schauen Sie sich auch dieses Video an, das den Sinn des neuen Mechanismus zur Verkettung von Zeichenfolgen gut erklärt: youtu.be/wIyeOaitmWM?t=37m58s
ZhekaKozlov
3
@ZhekaKozlov Ich wünschte, ich könnte Ihren Kommentar zweimal abstimmen. Links, die von Leuten stammen, die all dies tatsächlich implementieren, sind die besten.
Eugene
2
@Nicolai: Das wäre großartig und eine bessere Antwort als jede andere hier (einschließlich meiner). Alle Teile meiner Antwort, die Sie einbeziehen möchten, fühlen Sie sich frei - wenn Sie (im Grunde) das Ganze als Teil der umfassenderen Antwort einbeziehen, lösche ich einfach meine. Wenn Sie meine Antwort nur ergänzen möchten, da sie gut sichtbar ist, habe ich sie zu einem Community-Wiki gemacht.
TJ Crowder

Antworten:

95

Der "alte" Weg gibt eine Reihe von StringBuilderorientierten Operationen aus. Betrachten Sie dieses Programm:

public class Example {
    public static void main(String[] args)
    {
        String result = args[0] + "-" + args[1] + "-" + args[2];
        System.out.println(result);
    }
}

Wenn wir das mit JDK 8 oder früher kompilieren und dann javap -c Exampleden Bytecode verwenden, sehen wir ungefähr Folgendes:

öffentliche Klasse Beispiel {
  öffentliches Beispiel ();
    Code:
       0: aload_0
       1: invokespecial # 1 // Methode java / lang / Object. "<Init>" :() V.
       4: zurück

  public static void main (java.lang.String []);
    Code:
       0: neue # 2 // Klasse java / lang / StringBuilder
       3: dup
       4: invokespecial # 3 // Methode java / lang / StringBuilder. "<Init>" :() V.
       7: aload_0
       8: iconst_0
       9: aaload
      10: invokevirtual # 4 // Methode java / lang / StringBuilder.append: (Ljava / lang / String;) Ljava / lang / StringBuilder;
      13: ldc # 5 // String -
      15: invokevirtual # 4 // Methode java / lang / StringBuilder.append: (Ljava / lang / String;) Ljava / lang / StringBuilder;
      18: aload_0
      19: iconst_1
      20: Aaload
      21: invokevirtual # 4 // Methode java / lang / StringBuilder.append: (Ljava / lang / String;) Ljava / lang / StringBuilder;
      24: ldc # 5 // String -
      26: invokevirtual # 4 // Methode java / lang / StringBuilder.append: (Ljava / lang / String;) Ljava / lang / StringBuilder;
      29: aload_0
      30: iconst_2
      31: Aaload
      32: invokevirtual # 4 // Methode java / lang / StringBuilder.append: (Ljava / lang / String;) Ljava / lang / StringBuilder;
      35: invokevirtual # 6 // Methode java / lang / StringBuilder.toString :() Ljava / lang / String;
      38: astore_1
      39: getstatic # 7 // Feld java / lang / System.out: Ljava / io / PrintStream;
      42: aload_1
      43: invokevirtual # 8 // Methode java / io / PrintStream.println: (Ljava / lang / String;) V.
      46: zurück
}}

Wie Sie sehen können, erstellt es ein StringBuilderund verwendet append. Dies ist bekanntermaßen ziemlich ineffizient, da die Standardkapazität des integrierten Puffers StringBuildernur 16 Zeichen beträgt und der Compiler nicht wissen kann, ob er im Voraus mehr zuweisen muss, sodass er letztendlich neu zuweisen muss. Es ist auch eine Reihe von Methodenaufrufen. (Beachten Sie, dass die JVM diese Anrufmuster manchmal erkennen und neu schreiben kann, um sie jedoch effizienter zu gestalten.)

Schauen wir uns an, was Java 9 generiert:

öffentliche Klasse Beispiel {
  öffentliches Beispiel ();
    Code:
       0: aload_0
       1: invokespecial # 1 // Methode java / lang / Object. "<Init>" :() V.
       4: zurück

  public static void main (java.lang.String []);
    Code:
       0: aload_0
       1: iconst_0
       2: Aaload
       3: aload_0
       4: iconst_1
       5: Aaload
       6: aload_0
       7: iconst_2
       8: Aaload
       9: invokedynamic # 2, 0 // InvokeDynamic # 0: makeConcatWithConstants: (Ljava / lang / String; Ljava / lang / String; Ljava / lang / String;) Ljava / lang / String;
      14: astore_1
      15: getstatic # 3 // Feld java / lang / System.out: Ljava / io / PrintStream;
      18: aload_1
      19: invokevirtual # 4 // Methode java / io / PrintStream.println: (Ljava / lang / String;) V.
      22: zurück
}}

Oh mein Gott, aber das ist kürzer. :-) Es macht einen einzigen Anruf makeConcatWithConstantsvon StringConcatFactory, was dies in seinem Javadoc sagt:

Methoden zur Erleichterung der Erstellung von String-Verkettungsmethoden, mit denen eine bekannte Anzahl von Argumenten bekannter Typen effizient verkettet werden kann, möglicherweise nach Typanpassung und teilweiser Auswertung von Argumenten. Diese Methoden werden normalerweise als Bootstrap-Methoden für invokedynamicAufrufstandorte verwendet, um die Funktion zur Verkettung von Zeichenfolgen in der Java-Programmiersprache zu unterstützen.

T.J. Crowder
quelle
41
Dies erinnert mich an eine Antwort, die ich vor fast 6 Jahren geschrieben habe: stackoverflow.com/a/7586780/330057 - Jemand fragte, ob er einen StringBuilder erstellen oder einfach nur alt +=in seiner for-Schleife verwenden sollte. Ich sagte ihnen, dass es darauf ankommt, aber vergessen wir nicht, dass sie irgendwann einen besseren Weg finden könnten, um Concat zu spielen. Die Schlüsselzeile ist wirklich die vorletzte Zeile:So by being smart, you have caused a performance hit when Java got smarter than you.
corsiKa
3
@corsiKa: LOL! Aber wow, es hat lange gedauert, bis ich dort ankam (ich meine nicht sechs Jahre, ich meine 22 oder so ... :-))
TJ Crowder
1
@supercat: Soweit ich weiß, gibt es mehrere Gründe, nicht zuletzt, dass das Erstellen eines varargs-Arrays zur Übergabe an eine Methode auf einem leistungskritischen Pfad nicht ideal ist. Durch die Verwendung von invokedynamickönnen zur Laufzeit verschiedene Verkettungsstrategien ausgewählt und an den ersten Aufruf gebunden werden, ohne dass der Aufwand für einen Methodenaufruf und eine Versandtabelle bei jedem Aufruf anfällt. mehr in Nicolais Artikel hier und in der JEP .
TJ Crowder
1
@supercat: Und dann ist da noch die Tatsache, dass es mit Nicht-Strings nicht gut funktioniert, da sie in String vorkonvertiert werden müssten, anstatt in das Endergebnis konvertiert zu werden. mehr Ineffizienz. Könnte es schaffen Object, aber dann müssten Sie alle Primitiven einpacken ... (Was Nicolai übrigens in seinem ausgezeichneten Artikel behandelt)
TJ Crowder
2
@supercat Ich bezog mich auf die bereits vorhandene String.concat(String)Methode, deren Implementierung das Array des resultierenden Strings direkt erstellt . Der Vorteil wird umstritten, wenn wir toString()auf beliebige Objekte zurückgreifen müssen . Ebenso muss der Aufrufer beim Aufrufen einer Methode, die ein Array akzeptiert, das Array erstellen und füllen, was den Gesamtnutzen verringert. Aber jetzt ist es irrelevant, da die neue Lösung im Grunde das ist, was Sie in Betracht gezogen haben, außer dass sie keinen Box-Overhead hat, keine Array-Erstellung benötigt und das Backend möglicherweise optimierte Handler für bestimmte Szenarien generiert.
Holger
20

Bevor wir auf die Details der invokedynamicImplementierung eingehen, die zur Optimierung der String-Verkettung verwendet wird, müssen meiner Meinung nach einige Hintergrundinformationen zu Was ist Invokedynamic und wie verwende ich es?

Die invokedynamic Anweisung vereinfacht und verbessert möglicherweise die Implementierung von Compilern und Laufzeitsystemen für dynamische Sprachen in der JVM . Dazu kann der Sprachimplementierer das benutzerdefinierte Verknüpfungsverhalten mit der invokedynamicAnweisung definieren, die die folgenden Schritte umfasst.


Ich würde wahrscheinlich versuchen, Sie mit den Änderungen, die für die Implementierung der Optimierung der String-Verkettung mitgebracht wurden, durch diese zu führen.

  • Definieren der Bootstrap - Methode : - Mit Java9, den Bootstrap - Methoden für die invokedynamicAufrufstellen, die String - Verkettung in erster Linie zu unterstützen makeConcatund makeConcatWithConstantswurden mit der eingeführten StringConcatFactoryUmsetzung.

    Die Verwendung von invokedynamic bietet eine Alternative zur Auswahl einer Übersetzungsstrategie bis zur Laufzeit. Die in verwendete Übersetzungsstrategie StringConcatFactoryähnelt der LambdaMetafactoryin der vorherigen Java-Version eingeführten. Darüber hinaus besteht eines der in der Frage genannten Ziele des JEP darin, diese Strategien weiter auszudehnen.

  • Angeben von Einträgen für konstante Pools : - Dies sind die zusätzlichen statischen Argumente für die invokedynamicAnweisung außer (1) MethodHandles.LookupObjekt, das eine Factory zum Erstellen von Methodenhandles im Kontext der invokedynamicAnweisung ist, (2) ein StringObjekt, der im dynamischen Aufruf erwähnte Methodenname Standort und (3) das MethodTypeObjekt, die aufgelöste Typensignatur des dynamischen Aufrufstandorts.

    Es sind bereits während der Verknüpfung des Codes verlinkt. Zur Laufzeit wird die Bootstrap-Methode ausgeführt und der eigentliche Code für die Verkettung verknüpft. Der invokedynamicAnruf wird mit einem entsprechenden invokestaticAnruf neu geschrieben. Dadurch wird die konstante Zeichenfolge aus dem konstanten Pool geladen. Die statischen Argumente der Bootstrap-Methode werden genutzt, um diese und andere Konstanten direkt an den Aufruf der Bootstrap-Methode zu übergeben.

  • Verwenden der aufgerufenen dynamischen Anweisung : - Dies bietet die Möglichkeit für eine verzögerte Verknüpfung, indem die Möglichkeit bereitgestellt wird, das Anrufziel während des ersten Aufrufs einmal zu booten. Die konkrete Idee zur Optimierung besteht darin, den gesamten StringBuilder.appendTanz durch einen einfachen invokedynamicAufruf zu ersetzen, der java.lang.invoke.StringConcatFactorydie Werte akzeptiert, die einer Verkettung bedürfen.

Der Vorschlag zur Verkettung von Zeichenfolgen angibt anhand eines Beispiels das Benchmarking der Anwendung mit Java9, bei dem eine ähnliche Methode wie @TJ Crowder kompiliert wird und der Unterschied im Bytecode zwischen den verschiedenen Implementierungen deutlich sichtbar ist.

Naman
quelle
17

Ich werde hier ein paar Details hinzufügen. Der wichtigste Teil ist, dass die Verkettung von Zeichenfolgen eine Laufzeitentscheidung ist und keine Kompilierungsentscheidung mehr . Daher kann es sich ändern, was bedeutet, dass Sie Ihren Code einmal gegen Java-9 kompiliert haben und die zugrunde liegende Implementierung nach Belieben ändern können, ohne dass eine erneute Kompilierung erforderlich ist.

Und der zweite Punkt ist, dass es im Moment Folgendes gibt 6 possible strategies for concatenation of String:

 private enum Strategy {
    /**
     * Bytecode generator, calling into {@link java.lang.StringBuilder}.
     */
    BC_SB,

    /**
     * Bytecode generator, calling into {@link java.lang.StringBuilder};
     * but trying to estimate the required storage.
     */
    BC_SB_SIZED,

    /**
     * Bytecode generator, calling into {@link java.lang.StringBuilder};
     * but computing the required storage exactly.
     */
    BC_SB_SIZED_EXACT,

    /**
     * MethodHandle-based generator, that in the end calls into {@link java.lang.StringBuilder}.
     * This strategy also tries to estimate the required storage.
     */
    MH_SB_SIZED,

    /**
     * MethodHandle-based generator, that in the end calls into {@link java.lang.StringBuilder}.
     * This strategy also estimate the required storage exactly.
     */
    MH_SB_SIZED_EXACT,

    /**
     * MethodHandle-based generator, that constructs its own byte[] array from
     * the arguments. It computes the required storage exactly.
     */
    MH_INLINE_SIZED_EXACT
}

Sie können eine davon über einen Parameter auswählen : -Djava.lang.invoke.stringConcat. Beachten Sie, dass dies StringBuilderimmer noch eine Option ist.

Eugene
quelle