Перейти к основному содержимому

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

· 7 мин. чтения

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. Вызвать динамический

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

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

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

3.1. Большая фотография

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

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

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

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

./42ef3d5cb385fada0b6f4ccf10a0d17f.png

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

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

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

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 , который указывает на логику конкатенации.

./dbab697024a038bfb76284caf74a0931.png

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

  • 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

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

  • Постоянные строки, такие как « 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 выполнить конкатенацию без изменения размера базового byte[]
  • BC_SB_SIZED_EXACT — это генератор байт-кода на основе StringBuilder , который точно вычисляет необходимое хранилище. Чтобы вычислить точный размер, сначала он преобразует все аргументы в String
  • MH_SB_SIZED основан на MethodHandle и в конечном итоге вызывает API StringBuilder для конкатенации. Эта стратегия также делает обоснованное предположение о требуемой мощности.
  • MH_SB_SIZED_EXACT аналогичен предыдущему, за исключением того, что он вычисляет необходимую емкость с полной точностью.
  • MH_INLINE_SIZE_EXACT заранее вычисляет требуемый объем памяти и напрямую поддерживает свой byte[] для хранения результата конкатенации . Эта стратегия является встроенной, потому что она повторяет то, что StringBuilder делает внутри .

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

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

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

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