«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."<init>":()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 для объединения метода входные данные, хотя мы не использовали StringBuilder в нашем коде.

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

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

3. Invoke Dynamic

Начиная с Java 9 и как часть JEP 280, конкатенация строк теперь выполняется с помощью invokedynamic.

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

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

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

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

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

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

    Подготовка сигнатуры функции, описывающей конкатенацию. Например, (String, int) -\u003e String Подготовка фактических аргументов для конкатенации. Например, если мы собираемся соединить «The answer is» и 42, то эти значения будут аргументами. Вызов метода начальной загрузки и передача ему сигнатуры функции, аргументов и нескольких других параметров. фактическая реализация для этой сигнатуры функции и инкапсуляция ее внутри MethodHandle. Вызов сгенерированной функции для создания окончательной объединенной строки

Indy Concat

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

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

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

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, этот использует значительно меньшее количество инструкций .

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

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

Чтобы увидеть эту логику времени выполнения, давайте проверим таблицу методов начальной загрузки (с помощью 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 метод бутстрапа. Метод начальной загрузки, в свою очередь, вернет ConstantCallSite, который указывает на логику конкатенации.

Indy

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

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

5. Рецепты

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

public class Person {

    private String firstName;
    private String lastName;

    // constructor

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

Чтобы сгенерировать строковое представление, 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

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

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

6. Варианты байт-кода

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

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

Чтобы использовать второй вариант, мы должны передать параметр -XDstringConcat=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

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

    Первый аргумент представляет часть перед переменной firstName — литерал «Person{firstName=\\»» Второй аргумент — значение поля firstName Третий аргумент — одинарная кавычка Четвертый аргумент — это часть перед следующей переменной — «, lastName=\\’» Пятый аргумент — поле lastName Шестой аргумент — одинарная кавычка Последний аргумент — закрывающая фигурная скобка

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

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

7. Стратегии

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

    «Стратегия BC_SB или «bytecode StringBuilder» генерирует один и тот же байт-код StringBuilder во время выполнения. Затем он загружает сгенерированный байт-код с помощью метода Unsafe.defineAnonymousClass. Стратегия BC_SB_SIZED попытается угадать необходимую емкость для StringBuilder. В остальном он идентичен предыдущему подходу. Предположение о емкости потенциально может помочь StringBuilder выполнить конкатенацию без изменения размера базового байта[]. BC_SB_SIZED_EXACT — это генератор байт-кода на основе StringBuilder, который точно вычисляет необходимое хранилище. Чтобы вычислить точный размер, сначала он преобразует все аргументы в String MH_SB_SIZED на основе MethodHandles и в конечном итоге вызывает API StringBuilder для конкатенации. Эта стратегия также делает обоснованное предположение о необходимой емкости. MH_SB_SIZED_EXACT похожа на предыдущую, за исключением того, что она вычисляет необходимую емкость с полной точностью. Эта стратегия является встроенной, поскольку она повторяет внутренние действия StringBuilder

Стратегия по умолчанию — MH_INLINE_SIZE_EXACT. Однако мы можем изменить эту стратегию, используя системное свойство -Djava.lang.invoke.stringConcat=\u003cstrategyName\u003e.

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

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

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