Кристиан Талингер десяток с лишним лет работает с виртуальными машинами 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 поставлен в десятку).
Вот здесь Кристиан обратил внимание слушателей на то, что в текущих дистрибутивах 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». Понятно, что волшебной таблетки не бывает, и всё всегда определяет реальное приложение — Кристиан и сам признаёт, что конкретные значения, конечно же, зависят от конкретных бенчмарков. Тем, кто решит попробовать Грааль, в любом случае придётся применять метод научного тыка, запускать и (наверняка) находить баги (а лучше их затем ещё править и оформлять пулл-реквесты в репо 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), погонять под нагрузкой и сравнить цифры. Результатами обещаю поделиться с сообществом.
Комментарии (6)
Hixon10
21.03.2018 22:26Я что-то так и не понял, зачем использовать Graal в реальной жизни.
Уже не на первой конфренции Кристиан (а также его коллега из Oracle — 2017.jokerconf.com/2017/talks/ghdvtsu3y60qai68waayi ) рассказывает про Грааль. Но, кажется, что в первую очередь это очень интересный инженерный проект. А вот, если говорить про типичную жизнь — Spring Boot, PostgreSQL, MongoDB, то в этом всём деле куда важнее архитектура системы, а узким местом почти наверное будет диск/сеть хранилищ.
Моя главная мысля — про GC, VM, Компиляторы очень интересно слушать рассказы и читать статьи. Но в реальной жизни обычно хватает выбранных по умолчанию тулчейнов, возможно, с некоторыми конфиграми.olegchir
21.03.2018 23:04В реальной жизни обычных проектов с перфомансом всё настолько плохо, что его можно улучшить и другими способами. Например, заменив все вручную написанные сортировки пузырьком на что-нибудь другое :-)
Оправдание «база тормозит» годится только до тех пор, пока действительно не припрёт. Например, можно взять базу, написанную на Java, и ускорять Граалем её. Cassandra, например, или Apache Ignite, или ещё что-нибудь.
Есть классически CPU-intensive задачи, не всё же круды с веб-интерфейсом писать. Если жизнь состоит только из написания крудов — может, пора что-то менять?
olegchir
21.03.2018 23:10> Я что-то так и не понял, зачем использовать Graal в реальной жизни.
«Использовать» тут какое-то сильно больше слово. Зачем написать несколько дополнительных флагов при запуске `java`, чтобы бесплатно получить проценты производительности? Что, действительно так лениво вписать эти несколько чёртовых букв?Hixon10
21.03.2018 23:17Не, я хотел сказать несколько другую мысль.
Мне кажется, что «Java» — очень взрослая платформа, где тулчейн, который поставляется по умолчанию, в среднем лучше всего другого. Как только появляется новый GC, Компилятор, что-то ещё, который на всех измеряемых ворклоадах в среднем лучше, с течением времени он становится тем самым Дефолтом.
Это я к тому, что, раз Graal — не дефолт — openjdk.java.net/jeps/317 — то, вероятно, есть много ворклоадов, где он в среднем хуже. И, наверное, пока не стоит слепо «написать несколько дополнительных флагов».
Вы верно заметили, современные системы — это просторы для оптимизации. У Алексея есть замечательный доклад — Алексей Шипилёв — Перформанс: Что В Имени Тебе Моём?, в котором говорится, что до тюнинга уровня java-кода в реальных системах ой как трудно дойти: на каждом шагу пузырьковые сортировки, написанные за O(N^2).
olegchir
21.03.2018 22:37О, в нашем полку граалистов прибыло!
Вместе сделаем, чтобы хаб Java начал называться хабом Graal!
Regis
5 секунд на бенчмарк CPU? Это как-то не серьёзно.