JEP 348: Compiler Intrinsics for Java SE APIs
Intrinsics компилятора для API Java SE
| Автор | Brian Goetz |
| Ответственный | Vicente Arturo Romero Zaldivar |
| Тип | Feature |
| Область | SE |
| Статус | Closed / Withdrawn |
| Компонент | tools |
| Обсуждение | amber dash dev at openjdk dot java dot net |
| Рецензенты | Alex Buckley, Brian Goetz, Vicente Arturo Romero Zaldivar |
| Одобрен | Alex Buckley |
| Создан | 2018/06/25 21:23 |
| Обновлён | 2024/10/25 11:43 |
| Задача | 8205637 |
Аннотация
Дать компиляторам Java возможность применять новые стратегии генерации кода (intrinsification), чтобы повысить производительность некоторых методов Java SE.
Мотивация
В современных реализациях JVM JIT-компиляторы (Just-In-Time) отлично оптимизируют байт-код во время выполнения. Значительная часть байт-кода по своей природе «рутинная» — она перекладывает данные со стека в кучу и обратно — и может быть оптимизирована такими приёмами, как устранение упаковки и встраивание методов. Однако анализ, который JIT-компилятор может выполнить за разумное время и в разумном объёме памяти, ограничен, поэтому он может упустить некоторые возможности для оптимизации. К сожалению, то, как вызовы методов в исходном коде компилируются в байт-код, обычно повышает вероятность такого промаха.
Рассмотрим, например, вызов метода String::format (API. Первый аргумент — строка формата, например %s %d, за которой следуют varargs любого типа. Компилятор Java генерирует байт-код, который упаковывает примитивные varargs, создаёт массив, инициализирует его и вызывает метод; байт-код тела метода выполняет эти шаги в обратном порядке, чтобы получить значения для подстановки по строке формата. К сожалению, тело метода слишком велико для встраивания, поэтому JIT-компилятор не может устранить ни упаковку и распаковку примитивных varargs, ни перекладывание varargs в массив и обратно. Хуже того, строка формата обычно является константным выражением, поэтому без встраивания она будет разбираться при каждом выполнении тела метода.
String::format важен, потому что это лаконичный и надёжный способ реализовать toString. Однако некоторые разработчики избегают его исключительно из соображений производительности и вместо него используют более многословные и подверженные ошибкам механизмы. Если оптимизировать вызов String::format, самый читаемый и удобный в сопровождении способ реализовать toString станет и самым производительным.
JEP 280 заменил трансляцию конкатенации строк на invokedynamic, в результате чего байт-код стал быстрее, выделений памяти стало меньше, а возможности оптимизации — единообразнее. Тот же приём можно применить к String::format (и к тесно связанным методам, таким как java.util.Formatter::format), компилируя вызов по альтернативной стратегии трансляции, которая настраивает байт-код для каждого конкретного вызова на основе информации, доступной во время компиляции, например статических типов и значений фактических аргументов.
Цели
Дать разработчикам JDK возможность (i) помечать методы как кандидаты на intrinsification компилятором Java и (ii) для этих методов-кандидатов реализовывать альтернативные трансляции вызовов, поведение которых соответствует спецификации метода.
Что не является целью
Не является целью разрешить intrinsification методов, объявленных вне основных модулей Java SE.
Описание
Традиционно компилятор Java транслирует вызов метода в исходном коде в одну из инструкций байт-кода invokevirtual, invokeinterface, invokespecial или invokestatic. Этот JEP позволяет компилятору использовать альтернативную трансляцию при вызове некоторых специально обозначенных методов API Java SE. Использование альтернативной трансляции называется intrinsification; про такой вызов говорят, что он подвергнут intrinsification.
Чтобы компилятор применил intrinsification к конкретному вызову данного метода, должно выполниться всё следующее:
- метод в месте своего объявления, в рамках своей спецификации, явно разрешает intrinsification;
- компилятор определяет, что к этому вызову можно применить intrinsification;
- компилятору известен intrinsic processor (обработчик intrinsics) для этого метода;
- intrinsic processor указывает альтернативную стратегию трансляции;
- компилятор генерирует байт-код, соответствующий указанной стратегии.
Разрешение intrinsification
Чтобы метод API Java SE разрешил intrinsification, он должен быть обозначен как intrinsic candidate (кандидат на intrinsification) с помощью аннотации @IntrinsicCandidate. Тогда компилятор может распознать вызов такого метода как допускающий intrinsification и может (но не обязан) передать решение о трансляции intrinsic processor.
Круг методов, которые могут разрешить intrinsification, ограничен из-за серьёзных опасений относительно широких последствий генерации нового байт-кода. Обозначить как intrinsic candidate можно только метод, экспортируемый модулем java.base, и только если это (i) экземплярный метод final-класса или (ii) static-метод, чтобы компилятор мог быть уверен в его поведении. Обозначение любого другого метода как intrinsic candidate будет проигнорировано.
(Может показаться, что подходит и final-экземплярный метод не-final-класса, но тело такого метода может вызывать не-final экземплярные методы того же класса; эти методы могут быть переопределены во время выполнения, поэтому поведение final-экземплярного метода недостаточно предсказуемо для intrinsification. Ещё менее предсказуемо поведение не-final-метода в не-final-классе, поэтому java.io.PrintStream::format не упоминается в этом JEP, несмотря на явное сходство с String::format.)
Тип аннотации IntrinsicCandidate входит в API Java SE и снабжён мета-аннотацией @Documented, чтобы подчеркнуть значимость применения этой аннотации.
Intrinsic processors
Компилятор Java может предоставлять механизм обнаружения intrinsic processors. Intrinsic processor указывает, какой метод или методы он способен обрабатывать; если компилятору не известен intrinsic processor для данного метода, вызовы этого метода не подвергаются intrinsification. Ради предсказуемости все intrinsic processors по умолчанию отключены и могут быть включены параметром командной строки javac -XDintrinsify=all. Если intrinsic processor не указал компилятору альтернативную трансляцию или компилятор решил проигнорировать такое указание, компилятор должен сгенерировать байт-код в соответствии с JLS 15.12.3.
Генерация альтернативного байт-кода
Intrinsic processor может указать альтернативную трансляцию для конкретного вызова данного метода, например замену на invokedynamic с заданным bootstrap-методом, замену на вызов другого метода, замену на загрузку константы и т. д. Тогда компилятор может сгенерировать для этой трансляции точный байт-код вместо традиционного.
Пример
Разберём, что даёт intrinsification String::format: устранение накладных расходов на упаковку и varargs, а также повторного анализа константных спецификаторов формата (первого аргумента). Рассмотрим следующий вызов:
String name = ...
int age = ...
String s = String.format("%s: %d", name, age);
Традиционно это приводит к упаковке age в Integer, выделению массива varargs, записи name и упакованного age в этот массив, а затем к разбору и интерпретации строки формата — при каждом вызове. Байт-код получается длинным:
0: ldc #2 // String John
2: astore_1
3: bipush 30
5: istore_2
6: ldc #3 // String %s: %d
8: iconst_2
9: anewarray #4 // class java/lang/Object
12: dup
13: iconst_0
14: aload_1
15: aastore
16: dup
17: iconst_1
18: iload_2
19: invokestatic #5 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
22: aastore
23: invokestatic #6 // Method java/lang/String.format:(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;
26: astore_3
27: return
Если спецификатор формата константный, а так бывает почти всегда, intrinsic processor может выбрать альтернативную трансляцию (обратите внимание, что ни name, ни age не обязаны быть константными переменными):
String s = name + ": " + Integer.toString(age);
Получив такую трансляцию, компилятор может оптимизировать её до invokedynamic с помощью механизмов JEP 280, и получается следующий байт-код:
0: ldc #2 // String John
2: astore_1
3: bipush 30
5: istore_2
6: aload_1
7: iload_2
8: invokedynamic #3, 0 // InvokeDynamic #0:format:(Ljava/lang/String;I)Ljava/lang/String;
13: astore_3
14: return
Помимо очевидного упрощения, этот байт-код работает в 30–50 раз быстрее традиционного.
Риски и допущения
При неправильной реализации альтернативная трансляция может оказаться не полностью совместимой по поведению со спецификацией или исходной реализацией.
Даже при правильной реализации альтернативная реализация может в будущем не успевать за изменениями исходной реализации.
Даже при правильной реализации и отслеживании изменений сопровождение методов — intrinsic candidates и их альтернативных трансляций усложняется, поскольку изменения, возможно, придётся вносить в двух местах, и по поведению они должны быть идентичны.
Нет гарантии, что производительность альтернативной реализации при каждом выполнении каждой программы на каждой машине будет выше той, которой достигла бы исходная реализация.
(В качестве примера трудностей с прогнозированием производительности рассмотрим метод Objects::hash. Ранняя версия этого JEP хвалила Objects::hash по тем же причинам, что и String::format: Objects::hash — лаконичный и надёжный способ реализовать hashCode. У Objects::hash похожая на String::format сигнатура), поэтому у байт-кода, генерируемого для его вызова, те же проблемы с производительностью, что и у String::format. Однако семантика хэширования и форматирования строк сильно различается, и эксперименты показали, что выигрыш в производительности от intrinsification Objects::hash гораздо меньше, чем от intrinsification String::format. Кроме того, выигрыш гораздо сильнее зависел от количества и значений фактических аргументов. Поэтому работа над intrinsification Objects::hash была прекращена.)