Рубрики
Без рубрики

Конкатенация строк с помощью Invoke Dynamic

Узнайте об относительно новой оптимизации Java: конкатенация строк с invokedynamic

Автор оригинала: Ali Dehghani.

1. Обзор

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

В этой статье мы рассмотрим одну из этих относительно новых оптимизаций: конкатенация строк с invokedynamic .

2. До Java 9

До Java 9 нетривиальные конкатенации строк были реализованы с помощью StringBuilder . Например, давайте рассмотрим следующий метод:

String concat(String s, int i) {
    return s + i;
}

Байт-код для этого простого кода выглядит следующим образом (с javap-c ):

java.lang.String concat(java.lang.String, int);
  Code:
     0: new           #2      // class StringBuilder
     3: dup
     4: invokespecial #3      // Method StringBuilder."":()V
     7: aload_0
     8: invokevirtual #4      // Method StringBuilder.append:(LString;)LStringBuilder;
    11: iload_1
    12: invokevirtual #5      // Method StringBuilder.append:(I)LStringBuilder;
    15: invokevirtual #6      // Method StringBuilder.toString:()LString;

Здесь компилятор Java 8 использует StringBuilder для объединения входных данных метода, e ven, хотя мы не использовали StringBuilder в нашем коде.

Честно говоря, объединение строк с помощью StringBuilder довольно эффективно и хорошо спроектировано.

Давайте посмотрим, как Java 9 изменяет эту реализацию и каковы мотивы для такого изменения.

3. Вызов динамического

Начиная с Java 9 и как часть JEP 280 , конкатенация строк теперь использует invokedynamic .

Основная мотивация, лежащая в основе изменений, заключается в более динамичной реализации . То есть можно изменить стратегию конкатенации без изменения байт-кода. Таким образом, клиенты могут извлечь выгоду из новой оптимизированной стратегии даже без перекомпиляции.

Есть и другие преимущества. Например, байт-код для invokedynamic является более элегантным, менее хрупким и меньшим.

3.1. Общая картина

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

В качестве примера предположим, что мы собираемся создать новую Строку , присоединив другую Строку к int . Мы можем думать об этом как о функции, которая принимает Строку и int , а затем возвращает объединенную строку .

Вот как работает новый подход для этого примера:

  • Подготовка сигнатуры функции, описывающей объединение. Например, (String, int) -> Строка
  • Подготовка фактических аргументов для конкатенации. Например, если мы собираемся объединить “Ответ есть” и 42, то эти значения будут аргументами
  • Вызов метода начальной загрузки и передача ему сигнатуры функции, аргументов и нескольких других параметров
  • Создание фактической реализации для этой сигнатуры функции и инкапсуляция ее в MethodHandle
  • Вызов сгенерированной функции для создания окончательной объединенной строки
Инди Конкат

Проще говоря, байт-код определяет спецификацию во время компиляции. Затем метод начальной загрузки связывает реализацию с этой спецификацией во время выполнения. Это, в свою очередь, позволит изменить реализацию, не касаясь байт-кода.

В этой статье мы расскажем о деталях, связанных с каждым из этих шагов.

Во-первых, давайте посмотрим, как работает связь с методом начальной загрузки.

4. Связь

Давайте посмотрим, как компилятор Java 9+ генерирует байт-код для того же метода:

java.lang.String concat(java.lang.String, int);
  Code:
     0: aload_0
     1: iload_1
     2: invokedynamic #7,  0   // InvokeDynamic #0:makeConcatWithConstants:(LString;I)LString;
     7: areturn

В отличие от наивного подхода StringBuilder , этот использует значительно меньшее количество инструкций .

В этом байт-коде подпись (String;I)String довольно интересна. Он принимает String и int ( I представляет int ) и возвращает объединенную строку. Это происходит потому, что метод соединяет одну строку и int вместе.

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

Чтобы увидеть эту логику выполнения, давайте проверим таблицу методов начальной загрузки (с помощью javap -c -v ):

BootstrapMethods:
  0: #25 REF_invokeStatic java/lang/invoke/StringConcatFactory.makeConcatWithConstants:
    (Ljava/lang/invoke/MethodHandles$Lookup;
     Ljava/lang/String;
     Ljava/lang/invoke/MethodType;
     Ljava/lang/String;
     [Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
    Method arguments:
      #31 \u0001\u0001

В этом случае, когда JVM впервые видит инструкцию invokedynamic , он вызывает метод makeConcatWithConstants bootstrap. Метод начальной загрузки, в свою очередь, вернет ConstantCallSite , который указывает на логику конкатенации.

Инди

Среди аргументов, переданных методу начальной загрузки, выделяются два:

  • Ljava/lang/invoke/MethodType представляет сигнатуру конкатенации строк. В этом случае это (String;I)String , так как мы объединяем целое число со строкой
  • \u0001\u0001 – это рецепт построения строки (подробнее об этом позже)

5. Рецепты

Чтобы лучше понять роль рецептов, давайте рассмотрим простой класс данных:

public class Person {

    private String firstName;
    private String lastName;

    // constructor

    @Override
    public String toString() {
        return "Person{" +
          "firstName='" + firstName + '\'' +
          ", lastName='" + lastName + '\'' +
          '}';
    }
}

Чтобы сгенерировать представление String , JVM передает поля FirstName и LastName в инструкцию invokedynamic в качестве аргументов:

 0: aload_0
 1: getfield      #7        // Field firstName:LString;
 4: aload_0
 5: getfield      #13       // Field lastName:LString;
 8: invokedynamic #16,  0   // InvokeDynamic #0:makeConcatWithConstants:(LString;LString;)L/String;
 13: areturn

На этот раз таблица методов начальной загрузки выглядит немного иначе:

BootstrapMethods:
  0: #28 REF_invokeStatic StringConcatFactory.makeConcatWithConstants // truncated
    Method arguments:
      #34 Person{firstName=\'\u0001\', lastName=\'\u0001\'} // The recipe

Как показано выше, рецепт представляет собой базовую структуру сцепленной Строки . Например, предыдущий рецепт состоит из:

  • Постоянные строки, такие как ” Person. Эти литеральные значения будут присутствовать в объединенной строке как есть
  • Два тега \u0001 для представления обычных аргументов. Они будут заменены фактическими аргументами, такими как FirstName

Мы можем думать о рецепте как о шаблоне String , содержащем как статические части, так и переменные заполнители.

Использование рецептов может значительно сократить количество аргументов, передаваемых методу начальной загрузки, так как нам нужно передать только все динамические аргументы плюс один рецепт.

6. Ароматизаторы байт-кода

Существует два варианта байт-кода для нового подхода к конкатенации. До сих пор мы знакомы с одним вкусом: вызов метода makeConcatWithConstants bootstrap и передача рецепта. Этот аромат, известный как indy с константами, является стандартным для Java 9.

Вместо того, чтобы использовать рецепт, второй аромат передает все в качестве аргументов . То есть он не различает постоянную и динамическую части и передает их все в качестве аргументов.

Чтобы использовать второй вариант, мы должны передать параметр -XD string Concat=indy компилятору Java . Например, если мы компилируем один и тот же класс Person с этим флагом, компилятор генерирует следующий байт-код:

public java.lang.String toString();
    Code:
       0: ldc           #16      // String Person{firstName=\'
       2: aload_0
       3: getfield      #7       // Field firstName:LString;
       6: bipush        39
       8: ldc           #18      // String , lastName=\'
      10: aload_0
      11: getfield      #13      // Field lastName:LString;
      14: bipush        39
      16: bipush        125
      18: invokedynamic #20,  0  // InvokeDynamic #0:makeConcat:(LString;LString;CLString;LString;CC)LString;
      23: areturn

На этот раз метод начальной загрузки – установить контакт . Кроме того, подпись конкатенации принимает семь аргументов. Каждый аргумент представляет одну часть из toString :

  • Первый аргумент представляет часть перед переменной FirstName/” Person{FirstName=\'” литерал Второй аргумент-это значение поля
  • first Name Третий аргумент-символ одинарной кавычки
  • Четвертый аргумент — это часть перед следующей переменной –
  • “, Фамилия=\'” Пятый аргумент-это поле
  • фамилия Шестой аргумент-это символ одинарной кавычки
  • Последний аргумент-закрывающая фигурная скобка

Таким образом, метод начальной загрузки содержит достаточно информации, чтобы связать соответствующую логику объединения.

Довольно интересно , что также можно вернуться в мир до Java 9 и использовать StringBuilder с параметром компилятора -XDstringConcat=inline .

7. Стратегии

Метод начальной загрузки в конечном итоге предоставляет MethodHandle , который указывает на фактическую логику конкатенации . На момент написания этой статьи существует шесть различных стратегий для генерации этой логики:

  • Стратегия BC_SB или “байт-код StringBuilder ” генерирует тот же StringBuilder байт-код во время выполнения. Затем он загружает сгенерированный байт-код с помощью метода Unsafe.defineAnonymousClass
  • BC_SB_SIZED стратегия попытается угадать необходимую емкость для StringBuilder . В остальном он идентичен предыдущему подходу. Угадывание емкости потенциально может помочь StringBuilder выполнить конкатенацию без изменения размера базового байта[]
  • BC_SB_SIZED_EXACT – это генератор байт-кода на основе StringBuilder , который точно вычисляет требуемое хранилище. Чтобы вычислить точный размер, сначала он преобразует все аргументы в String
  • MH_SB_SIZED основан на MethodHandle s и в конечном итоге вызывает StringBuilder API для объединения. Эта стратегия также дает обоснованное предположение о требуемой мощности
  • MH_SB_SIZED_EXACT аналогичен предыдущему, за исключением того, что он вычисляет необходимую емкость с полной точностью
  • MH_INLINE_SIZE_EXACT вычисляет требуемое хранилище заранее и непосредственно поддерживает его байт[] для хранения результата конкатенации . Эта стратегия является встроенной, потому что она повторяет то, что StringBuilder делает внутренне

Стратегия по умолчанию MH_INLINE_SIZE_EXACT . Однако мы можем изменить эту стратегию, используя –Java.lang.invoke.string Concat=<имя стратегии> системное свойство.

8. Заключение

В этой подробной статье мы рассмотрели, как реализована новая конкатенация String и преимущества использования такого подхода.

Для еще более подробного обсуждения рекомендуется проверить экспериментальные заметки или даже исходный код .