Хабрахабр

Отчёт с Java Virtual Machine Language Summit 2019

Как обычно, это было хардкорное мероприятие с техническими докладами, посвящёнными виртуальным машинам и языкам, которые на них работают. Сегодня закончился двенадцатый саммит JVM LS. Как обычно, желающих попасть сюда существенно больше, чем мест: количество участников не превышает 120. Как обычно, саммит проходил в Санта-Кларе, в кампусе компании Оракл. Как обычно, не было никакого маркетинга, только потроха.

Здесь можно не только послушать доклады, но и познакомиться поближе с известными людьми из мира JVM, поучаствовать в неформальных беседах, позадавать вопросы на воркшопах и вообще почувствовать себя причастным к великим свершениям. Для меня этот саммит уже третий, и каждый раз я посещаю его с большим удовольствием, несмотря на ужасный jetlag.

Большинство докладов выкладывают на YouTube практически сразу послед саммита. Если вы не попали на саммит, не беда. Чтобы было проще сориентироваться, я опишу здесь вкратце все доклады и воркшопы, на которых удалось поприсутствовать. Собственно они уже доступны.

Например, оказалось, что в Clojure принципиально занулять локальные переменные после последнего использования, потому что если в локальной переменной голова списка, который лениво генерируется, то при его обходе узлы, которые уже обошли, могут не собираться сборщиком мусора, и программа может упасть с OutOfMemory. Это не про особенности компиляции Future в языке Clojure, как многие подумали, а просто о развитии языка, тонкостях кодогенерации и проблемах, с которыми при этом сталкиваются. Вообще JIT-компилятор C2 сам отпускает переменные после последнего использования, но стандарт этого не гарантирует и, скажем, интерпретатор HotSpot этого не делает.

Ещё я узнал, что до недавнего времени Clojure таргетировался на JVM 6 и только недавно перешёл на JVM 8. Также интересно было узнать о реализации динамической диспетчеризации вызовов функций. Теперь авторы компилятора поглядывают на invokedynamic.

Год назад Алан и Рон уже рассказывали про этот проект, и тогда создалось впечатление, что всё идёт весьма хорошо и вот-вот будет готово. Проект Loom — это легковесные потоки (fibers) для Java. Конечно, оказалось, что надо утрясти много деталей. Тем не менее, официально в Java этот проект пока не вошёл и разрабатывается до сих пор в отдельном форке репозитория.

Однако, например, старый добрый synchronized-блок до сих пор не работает и, кажется, там не обойтись без серьёзного рефакторинга всей поддержки синхронизации в JVM. Многие стандартные API от ReentrantLock.lock до Socket.accept уже адаптированы к файберам: если такой вызов выполнится внутри файбера, то состояние исполнения будет сохранено, стек размотан и поток операционной системы освободится для других задач, пока не произойдёт событие, пробуждающее файбер (например, ReentrantLock.unlock). В обоих этих случаях ничего не взорвётся, но файбер не освободит поток. Ещё размотка стека не сработает, если между стартом файбера и точкой останова в стеке есть нативные фреймы.

Thread. Много вопросов относительно того, как Fiber соотносится со старым классом java.lang. Сейчас от этого отказались и делают его независимой сущностью, потому что эмулировать в каждом файбере всё поведение обычного потока довольно дорого. Год назад была идея сделать Fiber подклассом Thread. Но обманка будет довольно хорошо себя вести (хотя может замедлить работу). При этом Thread.currentThread() внутри файбера вернёт сгенерированную обманку, а не настоящий поток, в котором всё выполняется. Это может быть опасно, так как файбер может легко переехать на другой поток. Важная мысль в том, чтобы ни при каких обстоятельствах внутри файбера не выдать настоящий поток-носитель, на котором файбер выполняется. Обманка же сохранится.

Например, в Java 13 метод doPrivileged переписали с нативного кода полностью на Java, получив примерно 50-кратный прирост производительности. Любопытно, что участники проекта уже пропихнули несколько подготовительных изменений в основной репозиторий JDK, чтобы облегчить себе жизнь. Дело в том, что именно этот метод очень часто появляется в середине стека, а пока он был нативным, файберы с таким стеком не останавливались. Зачем это проекту Loom? Так или иначе, проект уже приносит пользу.

Надеемся, что в ближайшие года всё будет интегрировано. На странице проекта можно почитать документацию и скачать дерево исходников, а ещё есть бинарные сборки, которые уже сегодня можно брать и играться.

Здесь обсудили вкратце цели проекта и основные JEP'ы, в которых идёт работа — Pattern matching, Records и Sealed types. Параллельно шёл воркшоп про проект Loom, но я пошёл на Amber. Я рассказывал об этом на конференции Joker в прошлом году, в принципе ничего сильно нового не прозвучало. Затем всё обсуждение свалилось в частный вопрос скоупинга. Я пытался протолкнуть идею с неявными типами-объединениями вроде if(obj instanceof Integer x || obj instanceof Long x) use(x.longValue()), но энтузиазма не увидел.

Проект изначально написан как модуль LLVM для нативного кода, а теперь его адаптировали для HotSpot. Во всех отношениях замечательный проект от компании Google для поиска гонок по данным в виде чтения и записи одного и того же неволатильного поля или элемента массива из разных потоков без установки отношения happens-before. Это официальный проект OpenJDK со своим списком рассылки и репозиторием.

Кроме того, она находит гонки не только в Java-коде, но и в коде нативных библиотек. По утверждению авторов, штука сейчас вполне рабочая, можно собрать и играться. По заявлению авторов, ложных срабатываний TSan не даёт. Гонки в коде самой виртульаной машины не ищутся, потому что там все примитивы синхронизации написаны по-своему, и TSan их не умеет детектировать.

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

Докладчик не исключил такую возможность, но сказал, что они и так нашли огромное количество гонок, которые разгребать придётся очень долго. Кто-то спросил, не влияет ли на результат отключение JIT-компиляции, ведь какие-то гонки могут не проявляться на интерпретаторе. Так что будьте осторожны, запуская ваш проект под TSan: вы можете узнать неприятную правду.

Впрочем, движения всё более серьёзные. Все ждут value-типов в джаве, но никто не знает, когда они появятся. В текущих планах полная Вальгалла наступит на майлстоуне L100, но авторы всё же полны оптимизма и считают, что сделано более двух процентов. Уже сейчас имеются тестовые бинарные сборки с текущим майлстоуном L2.

Экземпляры таких классов могут встраиваться в другие объекты, а также возможны плоские массивы, содержащие экземпляры инлайн-классов. Итак, с точки зрения языка мы имеем классы с модификатором inline, которые особым образом обрабатываются виртуальной машиной. Записать null в переменную такого типа, конечно, не получится. У экземпляра нет заголовка, а значит, нет идентичности, хэшкод считается по полям, == тоже по полям, попытка синхронизации или Object.wait() на таком классе вызовет IllegalMonitorStateException. Впрочем, авторы предлагают альтернативу: если у вас объявлен inline-класс Point, то можно объявить поле или переменную типа (сюрприз-сюрприз!) Point?, и тогда будет полноценный объект в куче (вроде боксинга) с заголовком, идентичностью, и null туда впишется.

Тем не менее картина вырисовывается, и просвет виден. Серьёзными открытыми вопросами остаётся специализация дженериков и миграция существующих классов (например, Optional) в inline-класс, чтобы не сломать существующий код (да-да, люди записывают null в переменные типа Optional).

Net. Для меня было сюрпризом, что тот самый Нил Гафтер, соавтор оригинальных Java-паззлеров, теперь работает в Микрософте над рантаймом . Net) на JVM LS. Также сюрпризом было увидеть доклад про CLR (так называется рантайм . В докладе рассказывается про разновидности ссылок и указателей в CLR, про инструкции байткода, используемые для value-типов, про то как красиво специализируются обобщённые функции вроде reduce. Но познакомиться с опытом коллег из других миров всегда полезно. Net — интероп с нативным кодом. Интересно было узнать, что одна из целей value-типов в . В JVM такой задачи никогда не стояло, а что делать с нативным интеропом — смотрите ниже. Из-за этого расположение полей в value-типах строго фиксировано и может быть спроецировано на сишную структуру без преобразований.

Опять же вопрос, почему до сих пор ничего не выпустили, если ещё год назад всё выглядело вполне неплохо. Опять же обновление прошлогоднего доклада.

В векторы можно загружать данные из массивов, выполнять над ними операции вроде поэлементного умножения и закидывать назад. Вектор представляет собой совокупность из нескольких чисел, которая в железе может быть представима одним векторным регистром вроде zmm0 для AVX512. Количество операций просто огромно. Все операции, для которых есть инструкции процессора, интринсифицируются JIT-компилятором в эти инструкции. Промежуточные объекты Vector в идеале не создаются, работает escape-анализ. Если чего-то нет, используется альтернативная медленная реализация. Всякие стандартные вычислительные алгоритмы векторизуются на ура, используя всю мощь вашего процессора.

Эти векторы просто обязаны быть инлайн-классами, тогда все проблемы исчезнут. К сожалению, авторам тяжело без вальгаллы: escape-анализ хрупок и может легко не сработать. Оно кажется существенно более готовым. Непонятно, могут ли вообще выпустить это API до первой версии вальгаллы. Там много повторяющихся кусков для разных размеров регистров и разных типов данных, поэтому большая часть кода генерируется из шаблонов и поддерживать это больно. Ещё в числе проблем называют трудности с поддержкой кода.

В Java нет перегрузки операторов, поэтому выглядит математика уродливо: вместо max(va-vb*42, 0) приходится писать va.lanewise(SUB, vb.lanewise(MUL, 42)).lanewise(MAX, 0). Использование пока тоже неидеально. Тогда бы можно было генерировать кастомную операцию по лямбде вроде MYOP = binOp((va, vb) -> max(va-vb*42, 0)) и использовать её. Было бы красиво иметь доступ к AST лямбд как в C#.

Второй день проходил под флагом компиляции.

Проблемы есть всегда: JIT — это медленный стартап, потому что разогрев; затраты CPU на компиляцию. Сотрудник IBM, участник проекта JVM OpenJ9 рассказывает про их опыт JIT и AOT-компиляции. Часть проблем можно решить, объединив подходы: начав с AOT-компилированного кода и добивая потом JIT-ом. AOT — неоптимальная производительность из-за отсутствия профиля (профилировать можно, но нетривиально и не всегда профиль при компиляции совпадает с профилем при исполнении), сложнее использование, привязка к целевой платформе, к ОС, к сборщику мусора. Если у вас много виртуальных машин (привет, микросервисы), они все обращаются к отдельному сервису — JIT-компилятору (да-да, JITaaS), где всё по-взрослому, оркестрация, балансировка нагрузки. Хорошая альтернатива всему этому — кэширующий JIT. Весьма часто он может отдать готовый код определённого метода, потому что этот метод уже компилировался на другой JVM. Этот сервис и компилирует. Это сильно улучшает разогрев, убирает потребление ресурсов с вашего JVM-сервиса и вообще снижает суммарное потребление ресурсов.

К сожалению, не уловил, можно ли этим поиграться прямо сейчас или пока ещё это закрытая разработка. В общем, JITaaS может стать следующим баззвордом в мире JVM.

Точнее это не совсем Java-приложение. GraalVM Native Image — это Java-приложение, скомпилированное в нативный код, который выполняется без JVM (в отличие от модулей, собранных с помощью AOT-компилятора вроде jaotc). Можно рефлекшн и метод-хэндлы, но вам придётся при компиляции конкретно рассказать, какие классы и методы будут использоваться через рефлекшн. Для корректной работы ему нужен закрытый мир, то есть весь код должен быть виден на этапе компиляции, никаких Class.forName.

Многие классы инициализируются в процессе компиляции. Ещё забавная штука — инициализация классов. Это требуется, чтобы достичь лучшего качества компиляции: можно делать всякий constant folding, если значения статических полей известны компилятору. То есть, скажем, ваши статические поля будут по умолчанию вычисляться компилятором и результат будет записан в собранный образ, а при запуске приложения просто считан. А при сборке нативного приложения приходится ухищряться. У JIT-то всё хорошо, статическую инициализацию выполняет интерпретатор, а потом зная константы, можно компилировать. Так классы обычно инициализируются в порядке обращения к ним, а во время компиляции этот порядок неизвестен и возможна инициализация в другом. Это, конечно, приводит к весёлым психоделическим эффектам. При наличии циклических ссылок между инициализаторами классов можно увидеть разницу в поведении кода на JVM и в нативном образе.

Я, к сожалению, большую часть прослушал. Разбиралась всякая боль, связанная со сборщиками мусора. Здесь есть хорошая новость: в Java 13 добавляется новая опция -XX:SoftMaxHeapSize. Помню, что обсуждался возврат памяти ОС, в том числе всем опостылевший Xmx. Она задаёт лимит размера кучи, который не надо превышать за исключением экстренных ситуаций, когда по-другому не получается. Пока она поддерживается только коллектором ZGC, но G1 тоже может подтянуться. Тогда JVM будет держать себя в рамках большую часть времени, но при пиковой нагрузке всё же не кинет OutOfMemoryError, а возьмёт ещё памяти у ОС. Таким образом можно задать большой Xmx (скажем, равный размеру всей оперативной памяти) и какой-то разумный SoftMaxHeapSize. Когда нагрузка упадёт, память вернётся.

AOT-компиляция у них развивается давно, но изначально (ngen.exe) велась на целевой платформе, вроде как при первом запуске (если у вас винда, поищите файлы *.ni.dll в папке Windows). Мэй-Чин Цай из Микрософт рассказала об особенностях JIT и AOT компиляции в CLR. Соответственно если обновляется зависимость, все нативные модули надо перекомпилировать. Файлы получаются зависимые от версии локальной винды и даже от других DLL-ек. Это замедлило код, потому что вызовы к зависимостям теперь пришлось делать честно виртуальными. Во втором поколении (crossgen) появилась предкомпиляция авторами приложения и модули относительно независимые от железа и версии ОС и зависимостей. Далее разговоры шли про многоуровневую (tiered)-компиляцию (кажется, в CLR это в зачаточном уровне, в то время как в джаве уже не меньше десяти лет развивается) и про будущие планы сделать AOT по-настоящему кроссплатформенным. Данную проблему решили, подключая JIT и перекомпилируя горячий код в процессе работы приложения.

Они используют JVM для множества веб-сервисов. Коллеги из Alibaba представили свой подход к проблеме разогрева JVM. Однако проблема в том, что без запросов машина не прогревается: код, описывающий логику обработки запросов, не вызывается, а значит, не компилируется. В принципе сильно быстрый стартап не так важен, потому что балансировщик всегда может подождать, пока машина загрузится и только потом начать передавать ей запросы. Раньше они пытались это решить, закидывая поднявшийся сервис поддельными запросами до того как отправлять на него реальные запросы. Компилироваться он будет, когда придут первые запросы, то есть сколько бы балансировщик ни выжидал, будет провал производительности на первых запросах. Подход интересный, но довольно сложно сгенерировать такой поддельный поток, который бы вызвал компиляцию всего нужного кода.

В первой тысяче запросов один if всегда шёл по первой ветке, JIT-компилятор вообще выкинул вторую, вставив туда деоптимизационную ловушку, чтобы уменьшить размер кода. Отдельная проблема — деоптимизация. Пока снова наберётся статистика, пока метод скомпилируется компилятором C1, потом по полному профилю компилятором C2, пользователи будут испытывать замедление. Но 1001-й запрос пошёл во вторую ветку, сработала деоптимизация и весь метод ушёл в интерпретатор. А потом в том же методе может деоптимизироваться другой if, и всё пойдёт по новой.

Во время первого прогона сервиса в течение нескольких минут пишется лог компиляции: записывается, какие методы были скомпилированы и необходимая информация профилирования по веткам, типам и т. JWarmUp решает проблему следующим образом. Если этот сервис перезапускается, сразу после стартапа запускается инициализация всех классов из лога и компиляция логированных методов с учётом предыдущего профиля. д. К этому времени весь горячий код у неё уже будет скомпилирован. В итоге компилятор хорошенько поработает на старте, после чего балансировщик начнёт направлять запросы к этой JVM.

Запуск может получиться даже медленнее, потому что компилируется много методов, некоторые из которых может потребуются только через минуты после запуска. Стоит заметить, что здесь не решается проблема быстрого запуска. Зато лог получается переиспользуемый: в отличие от AOT можно поднять сервис на другой архитектуре или с другим сборщиком мусора и переиспользовать предыдущий лог.

Пока безуспешно, но работа движется. Авторы довольно давно пытаются просунуть JWarmUp в OpenJDK. Главное, что полноценный патч вполне себе доступен на сервере Code Review, поэтому вы можете легко применить его к исходникам HotSpot и собрать сами JVM с JWarmUp.

Тоже надстройка над OpenJDK, которая позволяет довольно легко перекидывать определённый Java-код на GPU, iGPU, FPGA или просто распараллелить на ядра своего процессора. Это исследовательская работа из Манчестера, но авторы утверждают, что проект уже кое-где внедрён. Правильно написанный Java-метод совершенно прозрачно уходит на соответствующее устройство. Для компиляции на GPU они используют GraalVM в который встроили свой бэкенд — TornadoJIT. Некоторые бенчмарки (например, дискретное преобразование Фурье) по сравнению с голой джавой ускоряются более чем в сто раз, что в принципе ожидаемо. Правда, говорят, компиляция на FPGA может занимать несколько часов, но если ваша задача считается месяц, то почему бы и нет. Проект полностью выложен на GitHub, там же можно ознакомиться с научными публикациями по теме.

Оказалось, что с тех пор сместился фокус. Всё та же песня — давний проект, каждый саммит презентация, год назад всё выглядело довольно готово, но релиза всё нет.

Все знают, насколько больно пользоваться JNI. Идея проекта в улучшенном интеропе с нативным кодом. Проект Panama сводит эту боль на нет: с помощью jextract по *.h-файлам нативной библиотеки генерируются Java-классы, которыми вполне удобно пользоваться, вызывая нативные методы. Очень больно. Вдобавок всё стало куда быстрее: оверхед на вызовы Java->native и native->Java упал в разы. На стороне Си/Си++ вообще не надо ни строчки писать. Чего ещё желать?

До сих пор рекомендуемый способ — это DirectByteBuffer, с которым множество проблем. Осталась проблема, которая стоит довольно давно — передача массивов данных в нативный код. Из-за этой и других проблем люди используют Unsafe, который при должном старании может легко положить всю виртуальную машину. Одна из самых серьёзных — неуправляемое время жизни (буфер исчезнет, когда сборщик мусора подберёт соответствующий Java-объект).

Аллокация, структурированные аксессоры, явное удаление. Это значит, нужен новый нормальный доступ к памяти за пределами Java-кучи. Вместо этого вы один раз описываете лэйаут этой структуры и затем делаете, например, VarHandle, который может прочитать все x, перепрыгивая через y. Структурированные аксессоры — это чтобы вам не пришлось самим считать смещения, если вам нужно записать, например, struct [5]. Кроме того, должен быть запрет на обращение к уже закрытой области. При этом, разумеется, всегда должна быть проверка границ, как в обычных Java-массивах. Короче, смотрите видео, очень интересно. А это оказывается нетривиальная задача, если мы хотим сохранить производительность на уровне Unsafe и разрешить доступ из нескольких потоков.

Основная его часть сегодня — компилятор Graal. Проект Метрополис объединяет все попытки переписать части JVM на Java. Раньше была проблема бутстрапа: грааль медленно стартовал, потому что его самого надо было JIT-компилировать или интерпретировать. За последние годы он развился очень неплохо и уже реально ходят разговоры о полноценной замене устаревающему C2. Но с АОТ грааль отъедает приличную часть кучи Java-приложения, которое может не очень хочет делиться своей кучей. Тогда появилась AOT-компиляция (да, основная цель проекта по AOT-компиляции — это бутстрап самого грааля). С пиковой производительностью кода, собранного граалем, есть ещё проблемы на некоторых бенчмарках. Сейчас научились превращать грааль в нативную библиотеку с помощью Graal Native Image, что в итоге позволило изолировать компилятор от общей кучи. Однако благодаря очень мощному инлайнингу и escape-анализу он просто рвёт C2 на функциональном коде, где создаётся много неизменяемых объектов и много маленьких функций. Например, грааль отстаёт от C2 по интринсикам и векторизации. Тем более, что в последних версиях JDK это совсем тривиально делается парой ключиков, всё уже есть в комплекте. Если вы пишете на Скале и всё ещё не используете грааль, бегом использовать.

Поэтому извините. Кевин объявил о новом проекте, но просил не рассказывать публично и не выкладывать запись его выступления на YouTube. Скажу только, что официальный анонс должен состояться в этом году.

Он утверждает, что у них в Stripe чуть ли не самая большая кодовая база на Ruby в мире и, конечно, хочется поддерживать её качество. Дмитрий рассказывал о системе Sorbet для языка (внезапно!) Ruby, которая позволяет выводить типы для методов Ruby и проверять их. Честно говоря, в детали я не вник.

В первом Remi Forax очень коротко и убедительно сказал, что джаве следует отказаться от проверяемых исключений, что это зло. Микро-доклады по пять минут без видеозаписи. Прошу прощения у всех, кому мой твит внушил ложные надежды:

Это был просто крик души Реми, а не какие-то планы Оракла на ближайший релиз, как многие подумали.

Там были некоторые примеры того, что они в Facebook уже делают — автоматическое исправление ошибок в коде с помощью getafix, супер-авто-дополнение, которое может целый цикл за вас написать, и так далее. И последний очень живой доклад с весёлыми слайдами про то что скоро ML и AI будут делать за нас всю работу, и программисты станут не нужны. Дуальные числа, автоматическое дифференцирование, вот это вот всё. Вторая половина доклада была кратким описанием математических основ глубинного обучения. К сожалению, доклад вышел немного скомканым, потому что он явно рассчитан на большее время.

Получилось просто прекрасно. На этом саммит закончился. Завтра же начинается OpenJDK Committer Workshop.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть