W jaki sposób konkatenacja ciągów jest implementowana w Javie 9?

111

Jak napisano w JEP 280: Wskaż konkatenację ciągów :

Zmień statyczną Stringsekwencję kodu bajtowego -concatenation wygenerowaną przez program, javacaby używać invokedynamicwywołań funkcji biblioteki JDK. Umożliwi to przyszłą optymalizację Stringkonkatenacji bez konieczności dalszych zmian w kodzie bajtowym emitowanym przez javac.

Tutaj chcę zrozumieć, jakie jest użycie invokedynamicwywołań i czym różni się konkatenacja kodu bajtowego invokedynamic?

Mohit Tyagi
źródło
11
I napisał o tym jakiś czas temu - czy to pomaga, będę skraplać go do odpowiedzi.
Nicolai
10
Również spojrzeć na ten film, który ładnie wyjaśnia punkt nowego mechanizmu konkatenacji: youtu.be/wIyeOaitmWM?t=37m58s
ZhekaKozlov
3
@ZhekaKozlov Żałuję, że nie mogę dwukrotnie głosować za Twój komentarz, linki, które pochodzą od osób, które faktycznie to wszystko wdrażają, są najlepsze.
Eugene
2
@Nicolai: Byłoby wspaniale i byłoby lepszą odpowiedzią niż jakakolwiek inna tutaj (w tym moja). Wszelkie części mojej odpowiedzi, które chcesz uwzględnić, gdy to zrobisz, nie krępuj się - jeśli uwzględnisz (w zasadzie) całość jako część szerszej odpowiedzi, po prostu usunę moją. Alternatywnie, jeśli chcesz po prostu dodać do mojej odpowiedzi, ponieważ jest ona dość widoczna, utworzyłem ją jako wiki społeczności.
TJ Crowder

Odpowiedzi:

95

„Stary” sposób daje wiele StringBuilderzorientowanych operacji. Rozważ ten program:

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

Jeśli skompilujemy to z JDK 8 lub wcześniejszym, a następnie użyjemy javap -c Exampledo wyświetlenia kodu bajtowego, zobaczymy coś takiego:

klasa publiczna Przykład {
  public Przykład ();
    Kod:
       0: aload_0
       1: invokespecial # 1 // Metoda java / lang / Object. "<init>" :() V
       4: powrót

  public static void main (java.lang.String []);
    Kod:
       0: nowy # 2 // klasa java / lang / StringBuilder
       3: dup
       4: invokespecial # 3 // Metoda java / lang / StringBuilder. "<init>" :() V
       7: aload_0
       8: iconst_0
       9: aaload
      10: invokevirtual # 4 // Metoda java / lang / StringBuilder.append: (Ljava / lang / String;) Ljava / lang / StringBuilder;
      13: ldc # 5 // Ciąg -
      15: invokevirtual # 4 // Metoda java / lang / StringBuilder.append: (Ljava / lang / String;) Ljava / lang / StringBuilder;
      18: aload_0
      19: iconst_1
      20: aaload
      21: invokevirtual # 4 // Metoda java / lang / StringBuilder.append: (Ljava / lang / String;) Ljava / lang / StringBuilder;
      24: ldc # 5 // Ciąg -
      26: invokevirtual # 4 // Metoda java / lang / StringBuilder.append: (Ljava / lang / String;) Ljava / lang / StringBuilder;
      29: aload_0
      30: iconst_2
      31: aaload
      32: invokevirtual # 4 // Metoda java / lang / StringBuilder.append: (Ljava / lang / String;) Ljava / lang / StringBuilder;
      35: invokevirtual # 6 // Metoda java / lang / StringBuilder.toString :() Ljava / lang / String;
      38: astore_1
      39: getstatic # 7 // Pole java / lang / System.out: Ljava / io / PrintStream;
      42: aload_1
      43: invokevirtual # 8 // Metoda java / io / PrintStream.println: (Ljava / lang / String;) V
      46: powrót
}

Jak widać, tworzy a StringBuilderi używa append. Jest to znane jako dość nieefektywne, ponieważ domyślna pojemność wbudowanego bufora StringBuilderwynosi tylko 16 znaków i nie ma możliwości, aby kompilator wiedział, aby przydzielić więcej z wyprzedzeniem, więc w końcu musi ponownie przydzielić. To także kilka wywołań metod. (Należy jednak pamiętać, że JVM może czasami wykrywać i przepisywać te wzorce wywołań, aby były bardziej wydajne).

Spójrzmy, co generuje Java 9:

klasa publiczna Przykład {
  public Przykład ();
    Kod:
       0: aload_0
       1: invokespecial # 1 // Metoda java / lang / Object. "<init>" :() V
       4: powrót

  public static void main (java.lang.String []);
    Kod:
       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 // Pole java / lang / System.out: Ljava / io / PrintStream;
      18: aload_1
      19: invokevirtual # 4 // Metoda java / io / PrintStream.println: (Ljava / lang / String;) V
      22: powrót
}

Ojej, ale to krócej. :-) Wykonuje pojedyncze wywołanie do makeConcatWithConstantsfrom StringConcatFactory, które mówi w swoim Javadoc:

Metody ułatwiające tworzenie metod konkatenacji ciągów znaków, które można wykorzystać do skutecznego łączenia znanej liczby argumentów znanych typów, prawdopodobnie po dostosowaniu typu i częściowej ocenie argumentów. Metody te są zwykle używane jako metody ładowania początkowego w invokedynamicwitrynach wywołań w celu obsługi funkcji łączenia ciągów w języku programowania Java.

T.J. Crowder
źródło
41
Przypomina mi to odpowiedź, którą napisałem prawie 6 lat temu do dnia: stackoverflow.com/a/7586780/330057 - Ktoś zapytał, czy powinien stworzyć StringBuilder, czy po prostu użyć zwykłego starego +=w pętli for. Powiedziałem im, że to zależy, ale nie zapominajmy, że kiedyś mogą znaleźć lepszy sposób na połączenie concat. Kluczowa linia jest tak naprawdę przedostatnią linią:So by being smart, you have caused a performance hit when Java got smarter than you.
corsiKa
3
@corsiKa: LOL! Ale wow, dotarcie tam zajęło dużo czasu (nie mam na myśli sześciu lat, mam na myśli 22 lub więcej ... :-))
TJ Crowder
1
@supercat: Jak rozumiem, jest kilka powodów, między innymi to, że tworzenie tablicy varargs do przekazania do metody na ścieżce krytycznej dla wydajności nie jest idealne. Ponadto użycie invokedynamicumożliwia wybranie różnych strategii konkatenacji w czasie wykonywania i powiązanie ich przy pierwszym wywołaniu, bez narzutu wywołania metody i tabeli wysyłania przy każdym wywołaniu; więcej w artykule Nicolai tutaj oraz w JEP .
TJ Crowder
1
@supercat: I jest też fakt, że nie grałby dobrze z nie-ciągami, ponieważ musiałyby zostać wstępnie przekonwertowane na String, a nie przekształcone w wynik końcowy; większa nieefektywność. Mógłby to zrobić Object, ale wtedy musiałbyś opakować wszystkie prymitywy ... (które Nicolai opisuje w swoim doskonałym artykule, przy okazji)
TJ Crowder
2
@supercat Odnosiłem się do już istniejącej String.concat(String)metody, której implementacja tworzy lokalną tablicę wynikowego ciągu. Zaleta staje się dyskusyjna, gdy musimy odwołać toString()się do dowolnych obiektów. Podobnie, podczas wywoływania metody akceptującej tablicę, obiekt wywołujący musi utworzyć i wypełnić tablicę, co zmniejsza ogólną korzyść. Ale teraz nie ma to znaczenia, ponieważ nowe rozwiązanie jest w zasadzie tym, co rozważałeś, z wyjątkiem tego, że nie ma narzutu związanego z boksowaniem, nie wymaga tworzenia tablicy, a zaplecze może generować zoptymalizowane procedury obsługi dla określonych scenariuszy.
Holger
20

Zanim przejdziemy do szczegółów invokedynamicimplementacji używanej do optymalizacji konkatenacji ciągów, moim zdaniem, należy się trochę zapoznać z czym jest wywoływana dynamika i jak z niej korzystać?

W invokedynamic upraszcza instruktażowe i potencjalnie zwiększa implementacji kompilatorów i systemów uruchomieniowych dla dynamicznych języków na JVM . Czyni to, umożliwiając implementatorowi języka zdefiniowanie niestandardowego zachowania powiązania z invokedynamicinstrukcją, która obejmuje następujące kroki.


Prawdopodobnie spróbuję przeprowadzić Cię przez te wszystkie zmiany ze zmianami, które zostały wprowadzone w celu implementacji optymalizacji konkatenacji ciągów.

  • Definiowanie metody ładowania początkowego: - W Javie9 metody ładowania początkowego dla invokedynamicwitryn wywołujących obsługują przede wszystkim konkatenację ciągów makeConcati makeConcatWithConstantszostały wprowadzone wraz z StringConcatFactoryimplementacją.

    Użycie invokedynamic stanowi alternatywę dla wyboru strategii tłumaczenia do czasu wykonania. Strategia tłumaczenia używana w programie StringConcatFactoryjest podobna do tej, LambdaMetafactoryktóra została wprowadzona w poprzedniej wersji Java. Dodatkowo jednym z celów JEP wspomnianego w pytaniu jest dalsze rozszerzenie tych strategii.

  • Specifying Constant Pool Entries : - Są to dodatkowe statyczne argumenty invokedynamicinstrukcji inne niż (1) MethodHandles.Lookupobiekt będący fabryką do tworzenia uchwytów metod w kontekście invokedynamicinstrukcji, (2) Stringobiekt, nazwa metody wymieniona w wywołaniu dynamicznym site i (3) MethodTypeobiekt, rozpoznana sygnatura typu witryny wywołania dynamicznego.

    Podczas łączenia kodu są już linki. W czasie wykonywania metoda bootstrap działa i łączy w rzeczywistym kodzie wykonującym konkatenację. Przepisuje invokedynamicwywołanie odpowiednim invokestaticwywołaniem. Spowoduje to załadowanie stałego ciągu z puli stałej, a statyczne argumenty metody ładowania początkowego są wykorzystywane do przekazywania tych i innych stałych bezpośrednio do wywołania metody ładowania początkowego.

  • Korzystanie z instrukcji invokedynamic : - Zapewnia to ułatwienia dla leniwego łączenia, zapewniając środki do jednorazowego załadowania docelowego wywołania podczas początkowego wywołania. Konkretnym pomysłem na optymalizację jest tutaj zastąpienie całego StringBuilder.appendtańca prostym invokedynamicwywołaniem java.lang.invoke.StringConcatFactory, które zaakceptuje wartości wymagające konkatenacji.

Propozycja Indify String Concatenation przedstawia na przykładzie benchmarking aplikacji z Java9, gdzie skompilowano podobną metodę, jak współdzielona przez @TJ Crowder, a różnica w kodzie bajtowym jest dość widoczna między różnymi implementacjami.

Naman
źródło
17

Dodam tutaj trochę szczegółów. Główną częścią do uzyskania jest to, że sposób łączenia ciągów znaków jest decyzją wykonawczą, a nie kompilacją . W ten sposób może się zmienić, co oznacza, że ​​skompilowałeś swój kod raz z java-9 i może zmienić podstawową implementację w dowolny sposób, bez potrzeby ponownej kompilacji.

Po drugie, w tej chwili są 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
}

Można wybrać jedną z nich za pomocą parametru: -Djava.lang.invoke.stringConcat. Zauważ, że StringBuildernadal jest to opcja.

Eugene
źródło