Как объединение строк реализовано в Java 9?

111

Как написано в JEP 280: Указать конкатенацию строк :

Измените Stringпоследовательность байт-кода статической конкатенации, созданную с javacпомощью invokedynamicвызовов функций библиотеки JDK. Это позволит в будущем оптимизировать Stringконкатенацию, не требуя дальнейших изменений байт-кода, создаваемого javac.

Здесь я хочу понять, что такое использование invokedynamicвызовов и чем отличается конкатенация байт-кода от invokedynamic?

Мохит Тьяги
источник
11
Я писал об этом некоторое время назад - если это поможет, я сведу это в ответ.
Николай
10
Также посмотрите это видео, которое прекрасно объясняет суть нового механизма конкатенации строк: youtu.be/wIyeOaitmWM?t=37m58s
ZhekaKozlov 01
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версии, а затем воспользуемся им для просмотра байт-кода, мы увидим что-то вроде этого:

public class Example {
  общедоступный пример ();
    Код:
       0: aload_0
       1: invokespecial # 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: аалоад
      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: аалоад
      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:

public class Example {
  общедоступный пример ();
    Код:
       0: aload_0
       1: invokespecial # 1 // Метод java / lang / Object. "<init>" :() V
       4: возврат

  public static void main (java.lang.String []);
    Код:
       0: aload_0
       1: iconst_0
       2: aaload
       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 // Поле java / lang / System.out: Ljava / io / PrintStream;
      18: aload_1
      19: invokevirtual # 4 // Метод java / io / PrintStream.println: (Ljava / lang / String;) V
      22: возврат
}

О боже, но это короче. :-) Он выполняет единственный вызов makeConcatWithConstantsfrom StringConcatFactory, в Javadoc которого говорится следующее:

Методы для облегчения создания методов конкатенации строк, которые можно использовать для эффективного конкатенации известного количества аргументов известных типов, возможно, после адаптации типа и частичной оценки аргументов. Эти методы обычно используются в качестве методов начальной загрузки для invokedynamicсайтов вызовов для поддержки функции конкатенации строк языка программирования Java.

T.J. Crowder
источник
41
Это напоминает мне ответ, который я написал почти 6 лет назад в тот день: stackoverflow.com/a/7586780/330057 - Кто-то спросил, следует ли им создавать StringBuilder или просто использовать старый добрый +=в своем цикле for. Я сказал им, что это зависит от обстоятельств, но давайте не будем забывать, что они могут найти лучший способ связать конкатенацию в будущем. Ключевая строка - это действительно предпоследняя строка:So by being smart, you have caused a performance hit when Java got smarter than you.
corsiKa 01
3
@corsiKa: LOL! Но вау, это заняло много времени (я не имею в виду шесть лет, я имею в виду 22 или около того ... :-))
TJ Crowder
1
@supercat: Насколько я понимаю, есть несколько причин, не в последнюю очередь то, что создание массива varargs для передачи методу по пути, критически важному для производительности, не идеален. Кроме того, использование invokedynamicпозволяет выбирать разные стратегии конкатенации во время выполнения и связывать их при первом вызове без дополнительных затрат на вызов метода и таблицу отправки при каждом вызове; подробнее в статье Николая здесь и в JEP .
TJ Crowder
1
@supercat: И еще тот факт, что он не будет хорошо работать с не-строками, поскольку их нужно будет предварительно преобразовать в String, а не преобразовать в окончательный результат; больше неэффективности. Мог бы сделать это Object, но тогда вам придется боксировать все примитивы ... (что Николай освещает в своей превосходной статье, кстати.)
TJ Crowder
2
@supercat Я имел в виду уже существующий String.concat(String)метод, реализация которого создает массив результирующих строк на месте. Преимущество становится спорным, когда мы должны вызывать toString()произвольные объекты. Аналогично, при вызове метода, принимающего массив, вызывающая сторона должна создать и заполнить массив, что снижает общую выгоду. Но теперь это не имеет значения, поскольку новое решение в основном то, что вы рассматривали, за исключением того, что оно не имеет накладных расходов, не требует создания массива, а серверная часть может генерировать оптимизированные обработчики для определенных сценариев.
Holger
20

Прежде чем вдаваться в подробности invokedynamicреализации, используемой для оптимизации конкатенации строк, на мой взгляд, необходимо получить некоторые сведения о том, что invokedynamic и как его использовать?

Эта invokedynamic инструкция упрощает и потенциально улучшает реализации компиляторов и систем времени выполнения для динамических языков на JVM . Он делает это, позволяя разработчику языка определять настраиваемое поведение связывания с помощью invokedynamicинструкции, которая включает следующие шаги.


Я бы, вероятно, попытался рассказать вам об изменениях, которые были внесены для реализации оптимизации конкатенации строк.

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

    Использование invokedynamic предоставляет альтернативу для выбора стратегии перевода до времени выполнения. Стратегия перевода, используемая в StringConcatFactory, аналогична стратегии, LambdaMetafactoryпредставленной в предыдущей версии Java. Кроме того, одной из целей JEP, упомянутых в вопросе, является дальнейшее расширение этих стратегий.

  • Указание постоянных записей пула : - это дополнительные статические аргументы invokedynamicинструкции, кроме (1) MethodHandles.Lookupобъекта, который является фабрикой для создания дескрипторов методов в контексте invokedynamicинструкции, (2) Stringобъекта, имя метода, упомянутого в динамическом вызове site и (3) MethodTypeобъект, сигнатура разрешенного типа динамического сайта вызова.

    Уже есть ссылки во время связывания кода. Во время выполнения запускается метод начальной загрузки и связывается с фактическим кодом, выполняя конкатенацию. Он перезаписывает invokedynamicвызов соответствующим invokestaticвызовом. Это загружает постоянную строку из пула констант, статические аргументы метода начальной загрузки используются для передачи этих и других констант прямо в вызов метода начальной загрузки.

  • Использование инструкции invokedynamic : - Это предлагает средства для ленивого связывания, предоставляя средства для однократной начальной загрузки цели вызова во время первоначального вызова. Конкретная идея оптимизации здесь состоит в том, чтобы заменить весь StringBuilder.appendтанец простым invokedynamicвызовом java.lang.invoke.StringConcatFactory, который примет значения, требующие объединения.

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

Naman
источник
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это все еще вариант.

Евгений
источник