Graal: как использовать новый JIT-компилятор JVM в реальной жизни

в 4:37, , рубрики: graal, java, java 9, JIT-компилятор, Блог компании Петер-Сервис

На главной сибирской Java-конференции JBreak-2018, проходившей в Новосибирске, Christian Thalinger из Twitter поделился практическим опытом использования Graal. Этот доклад мы слушали всей рабочей группой в полном составе. Вполне объяснимо, если учесть тот факт, что Graal по-прежнему считается смелым и потенциально опасным экспериментом (хотя очень похоже, что он войдёт в JDK 10). Было очень интересно узнать, как эта новинка проявляет себя в бою — да не где-нибудь, а в разработке такого уровня.

Graal: как использовать новый JIT-компилятор JVM в реальной жизни - 1

Кристиан Талингер десяток с лишним лет работает с виртуальными машинами Java, причём ключевой навык в его экспертизе — как раз JIT-компиляторы. Именно Кристиан внедрил Graal и стал инициатором его нынешнего (весьма, по словам Криса, активного) использования в продакшн-среде Twitter. И, если верить Талингеру, это нововведение сохраняет компании приличные деньги за счёт экономии железных ресурсов.

Вот в этом интервью с организаторами JBreak Кристиан доходчиво объясняет основы — что есть Graal и как с ним управляться. Ну а доклад в Новосибирске был более практико-ориентированным: основная его задача сводилась к тому, чтобы показать аудитории, как просто и безболезненно начать работать с Graal, и почему это стоит попробовать сделать.

Для начала — всё-таки пара теоретических вводных. Итак, что такое JIT — just-in-time компилятор? Для работы программы на Java требуется выполнить несколько шагов: сначала скомпилировать исходный код в инструкции для JVM — байткод, а затем запустить этот байткод в JVM. Здесь JVM выступает в качестве интерпретатора. JIT-компилятор был создан для ускорения работы Java-приложений: он занимается оптимизацией запускаемого байткода посредством перевода его в низкоуровневые машинные инструкции прямо в процессе выполнения программы.

В HotSpot/OpenJDK используются два уровня JIT-компиляции, реализованные на C++. Это C1 и C2 (известные также как клиентский и серверный). По умолчанию они работают совместно: сначала производится быстрая, но поверхностная оптимизация с помощью C1, а потом самые «горячие» методы дополнительно оптимизируются с помощью C2.

В Java 9 в рамках JEP-243 был реализован механизм для встраивания в JVM компилятора, написанного на Java. И это динамический компилятор — JVMCI (Java Virtual Machine Compiler Interface). Собственно, этот-то механизм и поддерживает Graal. Надо сказать, в Java 9 Graal уже был доступен в рамках JEP-295 — AOT-компиляция (Ahead-of-time) в JVM. Правда, хоть механизмы AOT-компиляции и используют Graal в качестве компилятора, в этом JEP указано, что изначально интегрирование кода Graal в JDK предполагается только в рамках платформы Linux/x64.

Таким образом, чтобы попробовать Graal, необходимо взять JDK с AOT и JMVCI. Причем если у вас есть необходимость запуска на платформах MacOS или Windows, придётся подождать выпуска Java 10 (в соответствующем тикете JDK-8172670 fix version поставлен в десятку).

Graal: как использовать новый JIT-компилятор JVM в реальной жизни - 2

Вот здесь Кристиан обратил внимание слушателей на то, что в текущих дистрибутивах JDK версия Graal, мягко говоря, устарела (то ли годичной давности, то ли еще младше). Но тут нам на помощь приходит модульность Java 9. Благодаря ней мы можем собрать из исходников Graal последнюю версию и встроить его в JVM посредством команды --upgrade-module-path. Так как разработка Graal была начата задолго до системы модулей, для его сборки используется специальный инструмент — mx, который в какой-то мере повторяет модульную систему Java. Инструмент работает на Python 2.7, все ссылки можно найти в репозитории Graal в GitHub.
То есть, мы сперва выкачиваем и устанавливаем mx, затем выкачиваем Graal и собираем его в модуль через mx, который потом заменит изначальный модуль в JDK.

На первый взгляд эти манипуляции могут показаться сложными и трудозатратными, но в действительности этот чёрт не так страшен. И в принципе возможность заменить версию Graal, не дожидаясь выпуска патча на JDK или даже новой JDK, лично мне кажется более чем удобной. По крайней мере, Кристиан показал, как сам собирал это всё вживую на машинах в облаке. При сборке Truffle, правда, возникла ошибка — нужны были какие-то дополнительные зависимости, установленные на машине. Но Graal собрался корректно и далее использовался именно в таком виде (из чего мы делаем вывод, что про Truffle можно и вовсе забыть: Graal вполне от него независим).

Далее: чтобы JVM начала использовать Graal, нужно дополнительно выставить 3 флага:

-XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler -XX:-EnableJVMCI

Поскольку по сути своей Graal представляет собой нормальное Java-приложение, ему тоже нужно скомпилироваться и подготовить себя к работе (так называемый bootstrapping). В режиме «по умолчанию» (on-demand) это происходит параллельно со стартом приложения, и в этом случае Graal использует C1 для оптимизации своего кода.

Существует также возможность явно запустить инициализацию перед стартом приложения, и при таком раскладе вы даже можете дать Graal указание соптимизировать самого себя. Однако это, как правило, занимает гораздо больше времени и не дает существенной выгоды. Грааль инициализируется чуть дольше, чем С1/С2, и активнее использует свободные процессорные мощности в силу того, что ему требуется скомпилировать больше классов. Но эти различия не так велики и практически нивелируются, теряясь в общем шуме при инициализации приложения.
Кроме того, поскольку Graal написан на Java, он использует heap для инициализации (в случае с C1/C2 память также используется, только через malloc). Основное потребление памяти приходится на старт приложения. И Graal, и C1/C2 используют при компиляции свободные ядра. Потребление памяти Граалем можно отследить, включив логгирование GC (на текущий момент изоляции хипа для инициализации Graal от основного хипа приложения не предусмотрено).

Ну вот, мы узнали, как всё это настроить — самое время понять, зачем. Какие, собственно, плюсы даст нам использование Graal?

Для ответа на этот вопрос Кристиан использовал практический пример. Он запустил пару бенчмарков из одного проекта, написанных на Scala: один активно работал с CPU, а другой уже более активно взаимодействовал с памятью. На бенчмарке, работавшем с CPU, при использовании Graal было заметно замедление в среднем на секунду за счет более долгого старта (сам бенчмарк выполнялся 5 секунд). Но вот на втором бенчмарке Graal показал вполне неплохой результат — ~20 секунд против ~28 на C1/C2. И это при том, что, как отметил Кристиан, на примере со Scala Graal работает не так хорошо, как мог бы (из-за динамической структуры генерируемого Scala байткода). То есть, можно надеяться, что в случае с чистым Java приложением всё должно быть ещё лучше.

Плюс к тому, при выводе логов GC было видно, что с Graal приложение производит гораздо меньше сборок мусора (примерно в 2 раза). Это связано с более эффективным escape analysis, который позволяет оптимизировать количество объектов, создаваемых на хипе.

Graal: как использовать новый JIT-компилятор JVM в реальной жизни - 3

Резюмируя свои личные впечатления от услышанного, скажу, что доклад показался мне достаточно всесторонним, и вовсе не несущим рекламного посыла в духе «все срочно переходите на Graal». Понятно, что волшебной таблетки не бывает, и всё всегда определяет реальное приложение — Кристиан и сам признаёт, что конкретные значения, конечно же, зависят от конкретных бенчмарков. Тем, кто решит попробовать Грааль, в любом случае придётся применять метод научного тыка, запускать и (наверняка) находить баги (а лучше их затем ещё править и оформлять пулл-реквесты в репо Graal).

Но в целом при нынешней тенденции к использованию микросервисов и stateless-приложений — а, как следствие, к более активному (и правильному) применению Young Gen — Graal выглядит очень неплохо.

Так что, если проект можно малой кровью перевести на Java 9 (или писать с нуля на ней), я бы точно попробовал Graal. И меня, к примеру, даже порадовало то, что весь упор в докладе был сделан именно на Graal как на JIT-компилятор — потому что в целом рядовому Java-разработчику именно в таком качестве он и требуется (то есть, без Truffel и прочего из GraalVM, что Oracle недавно объединила в некий фреймворк для разработки и рантайм для различных языков на базе JVM). Любопытно было бы ещё протестировать затраты памяти и посмотреть, насколько заметна разница между стандартным C1/C2 и Graal. С другой стороны, притом, что на приложение в наше время выделяется достаточно приличное количество памяти, и основной её объём расходуется при запуске (а сегодня это обычно инициализация и старт контейнера, который уже запускает само приложение), эти цифры, видимо, в любом случае не столь значимы.

Вот здесь можно скачать небольшую презентацию с доклада.

По правде говоря, лично меня идея настолько заинтересовала, что я планирую повторить все шаги, проделанные Кристианом, но попробовать прогнать уже непосредственно Java benchmark suites (например, DaCapo и SPECjvm2008 – в бенчмаркинге Java я ориентируюсь не настолько хорошо, так что буду признателен, если кто-то предложит более адекватные варианты в комментариях или лс). Ну и ближе к специфике работы – попробую набросать простое web-приложение (например, SpringBoot+Jetty+PostgreSQL), погонять под нагрузкой и сравнить цифры. Результатами обещаю поделиться с сообществом.

Автор: DualOne

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js