openjdk.ruOpenJDK на русском

JEP 160: Lambda-Form Representation for Method Handles

Представление method handles в виде лямбда-форм

ОтветственныйJohn Rose
ТипFeature
ОбластьImplementation
СтатусClosed / Delivered
Выпуск8
Компонентcore-libs / java.lang.invoke
Обсуждениеmlvm dash dev at openjdk dot java dot net
ТрудоёмкостьL
ДлительностьL
ОдобренMikael Vidstedt
Создан2012/03/15 20:00
Обновлён2026/01/09 22:16
Задача8046150

Аннотация

Улучшить реализацию method handles: заменить пути на языке ассемблера оптимизируемым промежуточным представлением, а затем переработать реализацию так, чтобы больше работы выполнялось в переносимом коде на Java, а не было жёстко встроено в JVM.

Цели

  • Улучшить производительность, качество и переносимость method handles и invokedynamic.
  • Сократить объём ассемблерного кода в JVM.
  • Снизить частоту нативных вызовов и других сложных передач управления при обработке method handles.
  • Повысить отдачу существующих механизмов оптимизации JVM для производительности JSR 292.
  • Убрать из JVM малоэффективные или сложные структуры, которые служат только JSR 292. (Например, убрать фазу «method handle walk» (обход method handles), основанную на распознавании шаблонов.)
  • Полная совместимость со спецификацией Java SE 7 для JSR 292.
  • Более качественная эталонная реализация JSR 292.

Что не является целью

Эта работа задумана как основа для будущей работы по оптимизации, которая будет вестись отдельно в JVM и в коде на Java. Поэтому проект будет успешным и при скромных улучшениях производительности и стабильности. Большой прирост производительности не требуется, если система станет явно проще, лучше структурированной и легче для оптимизации. Это, в свою очередь, будет ясно, если после упрощения кодовой базы Java 7 производительность не пострадает.

Мотивация

Реализация JSR 292 (method handles и invokedynamic) в JDK 7 опирается на большой объём написанного вручную ассемблерного кода, выполняющего преобразования аргументов method handles. Оптимизированный нативный код получается с помощью отдельного модуля, который обходит графы method handles с распознаванием шаблонов и преобразует их (внутри JIT-компилятора JVM) в промежуточное представление (байт-коды Java, а затем IR C1 или C2).

Такой архитектуры достаточно для многих применений, но у неё два недостатка. Во-первых, вызовы неконстантных method handles нельзя оптимизировать, потому что преобразование в IR с распознаванием шаблонов происходит только в местах вызова (таких как invokedynamic). При таких вызовах список аргументов приходится копировать из формата скомпилированного кода в формат интерпретатора, а затем выполнять написанный вручную ассемблерный код преобразования аргументов, что приводит к избыточному и неоптимизированному перемещению данных. Пользователи воспринимают это как «обрыв производительности».

Во-вторых, поскольку интерпретируемые и скомпилированные версии method handles используют разные механизмы выполнения (ассемблерный код и IR, сгенерированное по распознанным шаблонам), а перевод между представлениями несовершенен, скомпилированные method handles не всегда ведут себя так же, как интерпретируемые. В частности, периодически возникает NoClassDefFoundError, когда транслированные method handles становятся слишком большими и их байт-коды содержат слишком много символьных ссылок.

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

Как побочный эффект, удаление ассемблерного кода упростит перенос JVM на другие платформы.

В будущем вызовы неконстантных method handles станут более частыми, поскольку method handles входят в инфраструктуру «лямбд» Java, которые появятся в Java SE 8. В целом, по мере того как JSR 292 получает всё более широкое распространение, его производительность должна становиться более стабильной в более широком спектре сценариев использования.

Описание

Создать для method handles новое промежуточное представление, называемое лямбда-формой (lambda form), которое (a) непосредственно исполняемо и (b) напрямую и просто сводится к байт-кодам и/или IR JIT-компилятора. Реализовать все операции над method handles, а также места вызова invokedynamic, с помощью лямбда-форм.

Убрать весь ассемблерный код из methodHandles_<arch>.cpp, кроме небольшого числа ассемблерных инструкций (около 100) для субпримитивов, используемых лямбда-формами. (Для сравнения, JDK 7 генерирует заглушки method handles для многочисленных видимых пользователю преобразований аргументов, содержащие около 7000 ассемблерных инструкций.)

По возможности перенести логику реализации, специфичную для method handles, из JVM на уровень кода на Java. Положиться на то, что JIT-компиляторы будут активно оптимизировать лямбда-формы (или соответствующие им байт-коды) и их субпримитивы.

Лямбда-форма представляет собой последовательность слабо типизированных формальных параметров, за которой следует линейная, неветвящаяся последовательность выражений вызова методов. Каждое выражение состоит из метода (заданного произвольным константным method handle) и связанных с ним аргументов. Аргументом может быть произвольная константа или ссылка на предшествующий параметр или выражение внутри лямбда-формы.

Субпримитивы состоят из низкоуровневых адаптеров для непосредственного вызова method handles и для эмуляции каждого из четырёх режимов вызова (invokevirtual и т. д.).

Лямбда-форму можно в любой момент скомпилировать в компактные байт-коды Java и передать JVM для динамической загрузки. Лямбда-формы будут содержать собственные счётчики вызовов, что позволит им откладывать компиляцию, пока они не станут достаточно «горячими». Будущие версии JVM, возможно, смогут непосредственно выполнять, и/или компилировать, и/или профилировать лямбда-формы, что приведёт к новым вариациям на тему смешанного режима выполнения и оптимизации.

Чтобы максимально переиспользовать лямбда-формы и их скомпилированный код, система типов выражений лямбда-форм ослаблена до пяти так называемых базовых типов: reference, int, long, float и double. Это означает, что создавать их может только доверенный код на Java. Явные приведения типов и другие проверки сохраняют типобезопасность во всех точках входа, доступных пользователю.

В качестве сопутствующей оптимизации, которую новый фреймворк делает практичной, связанные (bound) method handles будут «развёрнуты» в небольшие структуры, содержащие их связанные значения, с небольшой упаковкой (boxing) или вовсе без неё. Эти структуры будут составляться и загружаться по мере необходимости.

Фреймворк генерации байт-кода для лямбда-форм и небольших структур с данными основан на библиотеке ASM. Хотя он предназначен для создания method handles, его легко расширить для создания объектов других типов. Поэтому эта работа, вероятно, станет основой для эффективного представления функциональных объектов «SAM», которые требуются проекту Lambda, а возможно, и других будущих конструкций, таких как объекты-кортежи или гибридные массивы.

Дополнительные заметки о дизайне хранятся в репозитории MLVM.

Альтернативы

Мы могли бы сохранить ассемблерный код и попытаться доработать существующий механизм распознавания шаблонов, приспособив его для компиляции отдельных method handles (при отсутствии мест вызова).

Недостатком стала бы возросшая сложность JIT-компиляторов JVM и (вероятно) быстрее убывающая отдача от работы по оптимизации. Сопровождение написанного вручную ассемблерного кода для всех целевых платформ увеличило бы затраты на каждую платформу, как для новых, так и для существующих. Особые типы стековых фреймов (так называемые рикошетные фреймы, ricochet frames), возможно, повысили бы вероятность ошибок в модулях, которым нужно обходить стек JVM.

Тестирование

Существующее тестирование с помощью модульных тестов (на основе jtreg) и «больших приложений» продолжится. Покрытие тестами будет постепенно улучшаться.

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

Зависимости

Это будет согласованное изменение в кодовых базах JVM и кода JDK на Java. (Изменения в коде на Java ограничены пакетами java.lang.invoke и sun.invoke.) Изменения должны развёртываться вместе.

Встроить поддержку работы разных версий друг с другом (старая JVM и новый JDK и/или старый JDK и новая JVM), по-видимому, непрактично. Это означает, что платформенно-зависимый ассемблерный код должен быть портирован, прежде чем на платформе можно будет запускать JDK 8.

Мы рассчитываем перенести этот код в JDK 7 (back-port). Это означает, что основную часть работы (например, рефакторинг JVM) нужно завершить до того, как в кодовой базе JDK 8 произойдут другие масштабные изменения, такие как крупные изменения метаданных (JEP 147) или перемещение метаданных (JEP 122).