Як реалізовано об'єднання рядків у Java 9?


111

Як написано в JEP 280: Вкажіть об'єднання рядків :

Зміна Stringпослідовності байт-кодів статичної конкатенації, згенерованих за javacдопомогою invokedynamicвикликів до функцій бібліотеки JDK. Це дасть можливість в майбутньому оптимізувати Stringконкатенацію, не вимагаючи подальших змін до байтового коду, виданого користувачем javac.

Тут я хочу зрозуміти, чим користуються invokedynamicдзвінки і чим відрізняється з'єднання байт-коду invokedynamic?


11
Я писав про це деякий час назад - якщо це допоможе, я згущую це у відповідь.
Миколай

10
Також перегляньте це відео, яке чудово пояснює точку нового механізму з’єднання рядків: youtu.be/wIyeOaitmWM?t=37m58s
ZhekaKozlov

3
@ZhekaKozlov Я хотів би, щоб я міг двічі проголосувати ваш коментар, найкращі посилання, які надходять від людей, які реалізують все це, є найкращими.
Євген

2
@Nicolai: Це було б чудово і було б кращою відповіддю, ніж будь-який інший тут (включаючи мою). Будь-які частини моєї відповіді, які ви хочете включити, коли ви це робите, не соромтесь - якщо ви включите (в основному) всю справу як частину більш широкої відповіді, я просто видалю свою. Крім того, якщо ви хочете просто додати мою відповідь, оскільки це досить видно, я зробив це вікі спільноти.
TJ Crowder

Відповіді:


95

"Старий" спосіб виводить купу StringBuilderорієнтованих операцій. Розглянемо цю програму:

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

Якщо ми компілюємо це з JDK 8 або новішою javap -c Exampleверсією, а потім використовуємо для перегляду байт-коду, ми бачимо щось подібне:

Приклад публічного класу {
  публічний приклад ();
    Код:
       0: aload_0
       1: викликспеціальний №1 // Метод java / lang / Object. "<init>" :() V
       4: повернення

  public static void main (java.lang.String []);
    Код:
       0: новий №2 // клас java / lang / StringBuilder
       3: дуб
       4: invokespecial №3 // Метод java / lang / StringBuilder. "<init>" :() V
       7: aload_0
       8: iconst_0
       9: завантаження
      10: invokevirtual # 4 // Метод java / lang / StringBuilder.append: (Ljava / lang / String;) Ljava / lang / StringBuilder;
      13: ldc № 5 // Рядок -
      15: invokevirtual # 4 // Метод java / lang / StringBuilder.append: (Ljava / lang / String;) Ljava / lang / StringBuilder;
      18: aload_0
      19: iconst_1
      20: aaload
      21: invokevirtual # 4 // Метод java / lang / StringBuilder.append: (Ljava / lang / String;) Ljava / lang / StringBuilder;
      24: ldc № 5 // Рядок -
      26: invokevirtual # 4 // Метод java / lang / StringBuilder.append: (Ljava / lang / String;) Ljava / lang / StringBuilder;
      29: aload_0
      30: iconst_2
      31: aaload
      32: invokevirtual # 4 // Метод java / lang / StringBuilder.append: (Ljava / lang / String;) Ljava / lang / StringBuilder;
      35: invokevirtual # 6 // Метод java / lang / StringBuilder.toString :() Ljava / lang / String;
      38: astore_1
      39: getstatic # 7 // Поле java / lang / System.out: Ljava / io / PrintStream;
      42: aload_1
      43: invokevirtual № 8 // Метод java / io / PrintStream.println: (Ljava / lang / String;) V
      46: повернення
}

Як бачите, він створює StringBuilderі використовує append. Це відоме досить неефективно, оскільки ємність вбудованого буфера за замовчуванням StringBuilderстановить лише 16 символів, і компілятор не може знати заздалегідь виділити більше, тому в кінцевому підсумку потрібно перерозподілити. Це також купа методів викликів. (Зауважте, що JVM іноді може виявити та переписати ці схеми дзвінків, щоб зробити їх ефективнішими.)

Давайте розглянемо, що створює Java 9:

Приклад публічного класу {
  публічний приклад ();
    Код:
       0: aload_0
       1: викликспеціальний №1 // Метод java / lang / Object. "<init>" :() V
       4: повернення

  public static void main (java.lang.String []);
    Код:
       0: aload_0
       1: iconst_0
       2: завантаження
       3: aload_0
       4: iconst_1
       5: завантаження
       6: aload_0
       7: iconst_2
       8: завантаження
       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 // Field java / lang / System.out: Ljava / io / PrintStream;
      18: aload_1
      19: invokevirtual # 4 // Метод java / io / PrintStream.println: (Ljava / lang / String;) V
      22: повернення
}

О, але це коротше. :-) Здійснює єдиний дзвінок makeConcatWithConstantsз StringConcatFactory, який говорить про це у своєму Javadoc:

Методи, що сприяють створенню методів конкатенації рядків, які можуть бути використані для ефективного об'єднання відомої кількості аргументів відомих типів, можливо після адаптації типу та часткової оцінки аргументів. Ці методи, як правило, використовуються як методи завантаження для invokedynamicвеб-сайтів для викликів для підтримки функції рядкового конкатенації мови програмування Java.


41
Це нагадує мені відповідь, яку я написав майже 6 років тому до цього дня: stackoverflow.com/a/7586780/330057 - Хтось запитав, чи варто робити StringBuilder чи просто використовувати звичайний старий +=у своєму циклі. Я сказав їм, що це залежить, але не будемо забувати, що вони можуть знайти кращий спосіб нанизувати конкомат колись вниз. Ключовий рядок - це передостанній рядок:So by being smart, you have caused a performance hit when Java got smarter than you.
corsiKa

3
@corsiKa: LOL! Але уау, знадобилося багато часу, щоб туди дістатися (я не маю на увазі шість років, я маю на увазі 22 або близько того ... :-))
TJ Crowder

1
@supercat: Наскільки я це розумію, є кілька причин, не в останню чергу те, що створення масиву varargs для переходу до методу на критично важливому шляху не є ідеальним. Крім того, використання invokedynamicдозволяє вибирати різні стратегії конкатенації під час виконання та прив'язувати до першого виклику, без накладних витрат таблиці викликів методу та диспетчеризації для кожного виклику; докладніше в статті Ніколая тут і в JEP .
TJ Crowder

1
@supercat: І тоді є факт, що він не буде добре грати з неструментами, оскільки їх потрібно було б попередньо перетворити на String, а не перетворити на кінцевий результат; більша неефективність. Могло це зробити Object, але тоді вам доведеться зібрати всі примітиви ... (про що Ніколай розповідає у своїй чудовій статті, btw.)
TJ Crowder

2
@supercat Я мав на увазі вже існуючий String.concat(String)метод, реалізація якого створює отриманий масив рядка на місці. Перевага стає суперечливою, коли нам доводиться посилатися toString()на довільні об’єкти. Так само, викликаючи метод, що приймає масив, абонент повинен створити і заповнити масив, що зменшує загальну вигоду. Але зараз це не має значення, оскільки нове рішення в основному - це те, що ви розглядали, за винятком того, що він не має боксерських витрат, не потребує створення масиву, і бекенд може генерувати оптимізовані обробники для конкретних сценаріїв.
Холгер

20

Перш ніж розібратися в деталях invokedynamicреалізації, що використовується для оптимізації об'єднання рядків, на мою думку, потрібно отримати деяку інформацію про те, що викликається динамікою, і як я можу це використовувати?

У invokedynamic спрощує інструкції і потенційно покращує реалізації компіляторів і систем часу виконання для динамічних мов на JVM . Це робиться, дозволяючи реалізатору мови визначити поведінку користувацьких зв’язків з invokedynamicінструкцією, яка включає в себе наступні кроки нижче.


Напевно, я б спробував провести вас через зміни, внесені для впровадження оптимізації конденсації String.

  • Визначення методу Bootstrap : - З Java9, методи завантаження для invokedynamicсайтів викликів, які підтримують конкатенацію рядків в першу чергу makeConcatі makeConcatWithConstantsбули введені разом з StringConcatFactoryреалізацією.

    Використання виклику динаміки надає альтернативу вибору стратегії перекладу до часу виконання. Використовувана стратегія перекладу StringConcatFactoryподібна до тієї LambdaMetafactory, що була введена в попередній версії Java. Окрім того, однією з цілей ПЕЕ, згаданих у питанні, є розширення цих стратегій.

  • Вказівка ​​постійних записів у пулі : - Це додаткові статичні аргументи до invokedynamicінструкції, відмінного від (1) MethodHandles.Lookupоб'єкта, який є фабрикою для створення методів обробки в контексті invokedynamicінструкції, (2) Stringоб'єкт, назва методу, згаданий у динамічному виклику сайт та (3) MethodTypeоб'єкт, розв'язаний тип підпису динамічного виклику сайту.

    Під час зв’язування коду вже пов'язано. Під час виконання запускається метод завантаження і посилається на фактичний код, здійснюючи конкатенацію. Він переписує invokedynamicдзвінок із відповідним invokestaticвикликом. Це завантажує постійний рядок з константного пулу, статичні аргументи методу завантаження використовуються для передачі цих та інших констант прямо до виклику методу завантаження.

  • Використання викликаної динамічної інструкції : - Це пропонує засоби для ледачого зв’язку, надаючи засоби для завантаження цілі виклику один раз під час початкового виклику. Конкретна ідея оптимізації тут полягає в тому, щоб замінити весь StringBuilder.appendтанець простим invokedynamicзакликом до java.lang.invoke.StringConcatFactory, який прийме значення в потребі конкатенації.

Пропозиція Indify String Concatenation вказує на прикладі тестування програми з Java9, де складається аналогічний метод, яким поділяється @TJ Crowder , і різниця в байт-коді досить помітна між різною реалізацією.


17

Я трохи додам тут трохи деталей. Основна частина, яку потрібно отримати, - це те, що конкатенація рядків - це рішення часу виконання, а не час компіляції . Таким чином, він може змінитися, тобто ви склали свій код один раз проти java-9, і він може змінити базову реалізацію, проте це заманеться без необхідності повторної компіляції.

І другий момент полягає в тому, що на даний момент є 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
}

Ви можете вибрати будь-якого з них з допомогою параметра: -Djava.lang.invoke.stringConcat. Зауважте, що StringBuilderце все-таки варіант.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.