JEP draft: Optimize Final Field Loads In Generated Code
Оптимизация загрузок final-полей в генерируемом коде
| Автор | vlivanov |
| Ответственный | Vladimir Ivanov |
| Тип | Feature |
| Область | JDK |
| Статус | Draft |
| Компонент | hotspot / compiler |
| Обсуждение | hotspot dash compiler dash dev at openjdk dot java dot net |
| Трудоёмкость | M |
| Длительность | M |
| Создан | 2015/07/23 13:16 |
| Обновлён | 2024/06/21 23:12 |
| Задача | 8132243 |
Аннотация
Включить в JIT-компиляторах оптимизации, выполняющие свёртку констант для загрузок final-полей в генерируемом коде.
Цели
Цель — разработать набор оптимизаций в JIT-компиляторах, выполняющих свёртку констант для загрузок final-полей в генерируемом коде. В ходе работы также будут изучены возможности ужесточить правила изменения final-полей во время выполнения.
Мотивация
JVMS и JMM дают ряд строгих гарантий относительно инициализации и видимости final-полей.
С точки зрения производительности заманчиво использовать эти гарантии и не загружать значения полей, которые не меняются, получая таким образом более эффективный код.
Более того, в некоторых сценариях оптимизации final-полей экземпляра критически важны для производительности. Например, JSR 292 (java.lang.invoke) во многом полагается на возможность свёртки констант при загрузке из final-полей экземпляра, чтобы добиться приемлемой производительности invokedynamic (пока в коде JVM для этого есть особые случаи).
Хотя JVM HotSpot уже оптимизирует загрузки из статических final-полей, с final-полями экземпляра она по-прежнему ведёт себя очень консервативно. Причина в том, что существуют сценарии (например, десериализация), в которых конструктор объекта пропускается, а значения final-полей записываются уже после создания объекта.
Неизменяемые объекты продвигаются как безопасные в конкурентных сценариях и становятся очень популярными, поэтому такие оптимизации должны пойти на пользу многим приложениям.
Описание
JVMS и так достаточно строга. На уровне байт-кода запись в final-поля экземпляра разрешена только в конструкторах (<init>), а запись в статические final-поля — только в статических инициализаторах (<clinit>).
Тем не менее существует ограниченный набор дополнительных сценариев, в которых изменение final-полей возможно. Есть 4 способа обойти ограничения и изменить значение final-поля во время выполнения:
-
Reflection API (через
Field.setAccessible()) -
java.lang.invoke(черезLookup.unreflect(), поскольку получить setter для final-поля невозможно) -
JNI(SetXXXField()) -
sun.misc.Unsafe(setXXX()/setXXXUnaligned())
API Unsafe намеренно оставлен за рамками. Он задуман как простой, хорошо структурированный набор строительных блоков для реализации низкоуровневых операций JVM и (независимо от этого) для доступа к некоторым возможностям аппаратной платформы во время выполнения. Обеспечить безопасность выполняемых операций — ответственность пользователя.
Что касается всех остальных случаев, JIT-компиляторы должны учитывать их все при оптимизации загрузок final-полей и либо отслеживать изменения, либо действовать консервативно и отказываться от оптимизаций.
Рассматриваются 3 подхода:
-
ужесточить правила изменения final-полей во время выполнения: запретить любые записи в final-поля после того, как объект полностью сконструирован;
-
молча выполнять
nullify(игнорировать и отбрасывать) недопустимые записи в final-поля; -
отслеживать в JVM все изменения final-полей и соответствующим образом адаптироваться.
Первый подход, ужесточение правил для недопустимых изменений final-полей, требует, чтобы JVM выбрасывала исключение, когда выполняется запись в final-поле корректно сконструированного объекта (подход fail-fast). Так поведение во время выполнения приводится в соответствие с JVMS.
Обычная последовательность байт-кода new/<init> гарантирует, что после завершения конструктора объект корректно сконструирован.
Это освобождает JVM от обязанности отслеживать все изменения final-полей и выбрасывать сгенерированный код, когда поле, оптимизированное ранее, меняет своё значение.
Однако существуют обоснованные сценарии использования, в которых ограничения JVMS следует ослабить (например, десериализация). Типичный сценарий — раздельное конструирование и публикация объекта. В таком случае последовательность new/<init> уже не работает, и для создания объектов используются нестандартные способы. Есть 3 способа создать экземпляр, не выполняя для него конструктор:
-
Unsafe.allocateInstance(Class<?>) -
ReflectionFactory.newConstructorForSerialization(Class<?>, Constructor<?>)(используется при десериализации) -
AllocObject(JNIEnv*, jclazz)в JNI
Эти функции должны создавать «slushy»-объекты — объекты, которые могут свободно меняться после создания. JVM должна разрешать изменение final-полей таких объектов и действовать консервативно при их оптимизации.
Свойство «slushy» можно хранить в виде флага в заголовке объекта.
Поскольку вызвать конструктор или вручную инициализировать объект — ответственность пользователя, нужна дополнительная операция («publish»/«freeze»), которая сообщает, что конструирование завершено, и сбрасывает флаг «slushy». Она даёт JVM знать, что конструирование объекта закончено (дальнейших изменений final-полей не планируется), и с этого момента JVM может ужесточить проверки и оптимизировать операции с final-полями.
JIT-компиляторы сверяются с этим флагом, решая, допустима ли свёртка final-полей. Reflection, JNI и MethodHandles проверяют флаг при попытке записи в final-поле и выбрасывают ошибку, если он не установлен.
Второй подход, молча выполнять nullifying для записей в final-поля корректно сконструированных объектов, в некоторых случаях допустим согласно JMM. Такое обнуление неотличимо от ситуации, когда запись происходит, но ни одно последующее чтение её не видит. Это возможно, если либо запись откладывается на неопределённый срок, либо все потоки (и скомпилированные методы) ранее выполнили кэширующее чтение исходного значения final-поля. Необходимо дополнительно исследовать, допускает ли JMM своего рода OOTA-кэширующее чтение исходного значения final-поля, поскольку потоки не обязаны физически выполнять такое кэширующее чтение заранее.
Наконец, при третьем подходе, если поведение во время выполнения не корректируется, JVM должна отслеживать все изменения final-полей и соответствующим образом адаптироваться, делая недействительным весь затронутый сгенерированный код.
В JNI, java.lang.invoke и Reflection API нужно добавить дополнительные проверки, чтобы уведомлять JVM, когда приложение пытается записать в final-поле. JVM должна отслеживать все зависимости сгенерированного кода от значений final-полей.
Риски и допущения
Из-за ужесточённых проверок в Reflection API существуют риски совместимости. Если приложение использует Reflection API для записи в final-поля, при попытке выполнить такие операции оно будет получать ошибки во время выполнения.
Затронуты внешние пользователи sun.misc.Unsafe, если они изменяют final-поля в корректно сконструированном объекте. Видимость таких изменений не гарантируется, т. е. так же, как сегодня при изменении статических final-полей.
Существует риск, что пользователь забудет выполнить операцию «publish» и объект навсегда останется в состоянии «slushy».
Этот риск можно снизить, предоставив в JVM и библиотеках диагностические средства для обнаружения «slushy»-объектов, оставшихся без контроля:
-
JVM можно снабдить развитыми проверками для их поиска;
-
можно реализовать альтернативную реализацию
sun.misc.Unsafe, которая обнаруживает изменения final-полей и проверяет бит «slushy».
Для оптимизации только на уровне JVM эксперименты показали значительный рост числа зависимостей, записываемых для сгенерированного кода во время выполнения. Это создаёт нагрузку на механизм отслеживания зависимостей в JVM как при записи (нужно больше места), так и при проверке (больше работы по перебору затронутого сгенерированного кода).
Следует измерить это влияние и рассмотреть дополнительные оптимизации (например, более эффективный поиск зависимостей отдельных объектов, отслеживание зависимостей на уровне классов или на уровне объектов), чтобы сократить как число зависимостей, так и накладные расходы на их отслеживание.
Зависимости
Нет.