Автор оригинала: 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 и преимущества использования такого подхода.
Для еще более подробного обсуждения рекомендуется проверить экспериментальные заметки или даже исходный код .