Как стать автором
Обновить
82.65

Valhalla — эпичный рефакторинг Java. Часть 3: наши первые результаты

Уровень сложностиПростой
Время на прочтение9 мин
Количество просмотров5.2K
Автор оригинала: Brian Goetz

Команда Spring АйО перевела и адаптировала доклад Брайана Гоетца «Valhalla — эпичный рефакторинг Java», и сегодня мы публикуем третью, финальную, часть.

  • В первой части серии было рассказано об истории и причинах появления проекта Valhalla.

  • Во второй части подробно разбирался вопрос о том, с какими фундаментальными сложностями команда столкнулась на пути к решению поставленных задач.

  • Третья и заключительная часть повествует о первых успехах команды и о том, как скоро мы сможем увидеть первые итоги проекта в действии.


Первые успехи в сфере производительности 

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

Ускорение происходит за счет нескольких факторов: скаляризации, улучшенной локальности и сокращения аллокаций. Например, при перемножении больших матриц производительность может увеличиться в 6 раз. При сложении массивов структур улучшение может составить от 3 до 12 раз, в зависимости от конфигурации, в первую очередь благодаря улучшенной локальности. Эти результаты впечатляют, и если вам не требуется identity, стоит подумать об отказе от нее — это может принести существенные выгоды.

Немного плохих новостей

К сожалению, текущая модель памяти Java имеет свои проблемы, и это не самые хорошие новости для нас. Достаточно давно, когда Java была еще относительно молодым языком, типы Long и Double страдали от проблемы с race condition. Если один поток записывал данные, а другой их считывал, не координируя действия, то порой можно было увидеть младшие 32 бита одной записи и старшие 32 бита другой. Это происходило потому, что в те времена большинство машин не поддерживали дешевые 64-битные атомарные записи, а для повышения производительности арифметики с длинными числами защитные механизмы не вводились.

Комментарий от редакции Spring АйО

Брайан говорит о времени, когда человечество в массе своей использовало x86 CPU (Intel ISA для 32-битных CPU), а не x86-64 (Intel ISA для 64-битных CPU). 

Проблема в том, что Long и Double в HotSpot занимали (и занимают) 64 бита. Поэтому, на 32-битном CPU атомарно поменять значение, которое занимает 64 бита, не используя специальные блокировки, нельзя. Эти блокировки, конечно, стоят производительности, и Java в свое время для 32-битных машин сделала выбор в данном случае в пользу перформанса, но, конечно, пожертвовала консистентностью.

Код с race condition на данных тогда считался поломанным, и ситуация воспринималась как типичная для Java: «Иногда ваши числа — полный мусор, извините!» Альтернатива тоже не устраивала: числовые операции должны были быть быстрыми, а многопоточный код для арифметики встречался не так часто. С другой стороны, если вы работаете с примитивами без синхронизации в разных потоках, то у вас уже в целом серьезные проблемы. Поэтому ранее, была ситуация, когда технически возвращался валидный Long, так что типобезопасность не нарушалась, но он мог содержать неконсистентное значение.

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

Комментарий от редакции Spring АйО

Под "разрывом" Брайан имеет в виду явление word tearing-а. Вот его описание в рамках JLS: https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.6

Для некоторых типов, таких как Complex или Double Complex, можно пойти на тот же компромисс, что и с Long, потому что приоритетом является упрощение работы с числами, а не защита от race condition. Однако для некоторых классов, таких как LongRange, инварианты очень важны. Может быть критично, если мы прочитаем LongRange и обнаружим, что нижний конец больше верхнего, так как это нарушает один из ключевых инвариантов этих классов.

Таким образом, возникает сложный компромисс, который команда Valhalla решила оставить на усмотрение авторов классов. Этот компромисс затрагивает производительность, целостность данных и включает множество особенностей, специфичных для домена. Поэтому классы, такие как Long, BigInteger или Complex, могут позволить себе разрыв, но для классов вроде Range, которые имеют инварианты, нет универсального решения. В таких случаях по умолчанию используется безопасный подход, но программист может отказаться от него, если это необходимо.

Сглаживание объектов и finality 

Многие из таких ограничений не применяются, если что-то является final “all the way up”, потому что в таком случае вы не получите записи данных в условиях race condition, так что нет нужды думать о проблеме разрыва. Это еще один хороший пример того, что, если вам не нужны мутации, отключите их, и вашей наградой может стать лучшая производительность.

Текущие результаты 

На данный момент можно уверенно сказать, что в общем случае value-объекты будут отлично работать в стеке и при оптимизации соглашений по вызовам. Однако, если у вас есть value-объект размером в миллион байт, то у нас просто нет такого количества регистров CPU, и, возможно, потребуется упаковать его в объект и передать ссылку. Однако это уже будет задачей JVM. Nullable value типы длиной менее 64 бит обычно становятся более плоскими в куче, non-nullable value типы размером 64 бита также могут быть упрощены. Что касается более крупных значений, они могут быть сделаны более плоскими, если они mutable “all the way up”, при условии, что вы выбрали послабления по атомарности. Когда компьютеры станут мощнее, что уже происходит, чипы AMD уже поддерживают эффективные атомарные операции для 128 бит, порог простоты представления объекта в памяти поднимется. 64 бита — это не магическое число, а просто текущий консенсус для большинства аппаратного обеспечения.

Чистая семантика как ключ к успеху

Это отличное напоминание о том, что Java всегда стремилась к оптимизации на уровне JVM. Философия Java заключается в том, что сглаживание должно быть частью оптимизации, и эту задачу должна брать на себя JVM. Разработчикам не нужно заниматься предварительной оптимизацией кода — JVM справится с этим гораздо лучше. Именно поэтому сглаживание идеально вписывается в эту модель. Valhalla помогает разработчикам выражать нужную семантику, исключая ненужные степени свободы, в то время как JVM берет на себя оптимизацию, обеспечивая высокую производительность.

История альтернативных подходов полна неудачных решений. Например, кто помнит ключевое слово register в C? Вскоре после его введения компиляторы стали настолько хороши, что начали игнорировать это ключевое слово, так как сами могли эффективнее управлять памятью, чем программисты.

Команда Valhalla не планирует вводить ключевое слово flat в язык Java. Вместо этого нужно найти правильные рычаги, которые будут удобны для пользователей и при этом позволят JVM выполнять необходимые оптимизации. На этом пути команда столкнулась с множеством ненадежных решений, которые не дали результатов. В конечном итоге они вернулись к основам и решили дать пользователям возможность отказываться от ненужных степеней свободы, что не только полезно с точки зрения семантики, но и может улучшить производительность. В итоге пришлось устранить некоторые уязвимости в протоколах инициализации.

И еще о совместимости

В результате, ситуация с совместимостью при миграции улучшилась. Было бы катастрофой, если бы value-классы не удалось было мигрировать в Integer или Optional. Не менее проблематичным стало бы введение non-nullable типов в Valhalla без доработки существующих библиотек для их поддержки. И на ранних этапах развития проекта подобные проблемы действительно возникали.

На данный момент главной проблемой совместимости при миграции остается Java Serialization API. Команде пришлось несколько раз переработать стек сериализации, чтобы поддержать value-классы. Дело в том, что при десериализации JVM создает пустые объекты и уже потом заполняет их значениями, но value объекты немутабельны, и JVM следит за тем, чтобы они оставались таковыми. Это создает проблемы для старого механизма сериализации.

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

И это все?

Многие могут задать неудобный вопрос: «Вы работали над этим 10 лет, и все, что вы можете показать — это value-классы и восклицательные знаки?» Да, в основном так и есть. Но это напоминает старую цитату Паскаля: «Извините, что я написал вам длинное письмо, у меня не было времени написать короткое». Потребовалось много времени, чтобы сделать очевидное очевидным, а ненужную сложность — ненужной.

Комментарий от редакции Spring АйО

Под "восклицательными знаками" подразумеваются возможные нотации non-nullable типов, например String!, предположительно (пока этого нет в языке, все может поменяться), будет обозначать ссылку на объект типа String, которая не может быть null.

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

В процессе упрощения были исключены все идеи, требовавшие серьезного вмешательства в JVM — новый байт-код, новые формы пула констант, новые дескрипторы типов. В итоге остались лишь несколько новых битов в классе, новый атрибут для null enforcement и несколько новых правил верификации. Именно на это ушло 10 лет: потребовалось найти правильные способы обозначить проблему, разработать подходящую пользовательскую модель, прорубить путь через первоначальные неверные идеи и определить баланс между тем, что должна делать JVM, и тем, что должен делать язык.

Чтобы доказать, что команда не сидела 10 лет сложа руки, ее участники попытались составить список проведенных исследований, которые в итоге не вошли в окончательную концепцию. Когда в списке оказалось около 60 пунктов, команда посчитала, что этого достаточно, и список остался незавершенным. На самом деле, эти 60 пунктов появились после того, как всего два сотрудника проверили архивы своих email-рассылок и выписали все, что было связано с такими исследованиями из дискуссионных цепочек. Выглядело это вот так:

Осталась важная задача — устранить необоснованные расхождения между int и Integer. После завершения проекта Valhalla они должны стать идентичными, но пока это не так. На данный момент устранено около 95% различий.

Сейчас примитивные типы нельзя использовать как reciver-ы, их нужно преобразовывать в классы-обертки. Они также не участвуют в ковариантных переопределениях, массивах и аргументах типов. Исправить это несложно — для этого уже подготовлен JEP.

Комментарий от редакции Spring АйО

Под "Receiver"-ом в контексте теории системы типов языков программирования, как правило, понимают некую сущность (Класс, Объект или т.п.), на чем была вызвана функция. 

Например, в Kotlin Int - это валидный receiver, т.к. вы прямо на Int можете вызывать функции, например:

val a = 1.toString()

Это совершенно корректный код на Kotlin. Но в Java  числовые литералы имеют имплицитный (т.е. неявный, вы его не объявляли) тип int - примитив. И Вы не можете в Java написать


1.toString()

Оно просто не будет компилироваться. Вы именно обязаны использовать Integer.


Там, где идет упоминание аргументов типов, речь идет о том, что данный Java код сейчас не возможен:


val list = new ArrayList<int>(32);

Т.к. int как аргумент типа использовать нельзя.

Ковариантное переопредление это фича, введенная в Java 5, вот ее описание: https://en.wikipedia.org/wiki/Covariant_return_type

Грубо говоря, сейчас речь о том, что если есть класс предок, у которого есть метод, возвращающий Integer, то на данный момент в Java мы не можем в дочернем классе переопределить этот метод и вернуть int. Valhalla может это исправить.

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

Но и это не все. Встает вопрос о перегрузке операторов — теме, которая в прошлом вызывала немало проблем в разных языках. Риски понятны, но команда Valhalla активно работает и над этим, добиваясь заметных успехов.

Остается работа и над системой обобщенных типов. Если у вас есть ArrayList параметризованый типом int, должно быть возможно использовать для него массив примитивного типа int, а не массив объектов. Долгое время эта задача оставалась в списке с низким приоритетом.

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

Подводя итоги   

За последние два года был достигнут значительный прогресс — создана вменяемая программная модель, позволяющая использовать оптимизации, обеспечивать совместимость при миграции и вносить изменения в язык и JVM с минимальными изменениями. Однако сложность реализации остается высокой, поэтому работа займет еще немало времени.

В настоящее время готовятся к релизу четыре JEP, и в будущем их станет больше. JEP 401 (JEP для самого проекта Valhalla) уже на пути к реализации. Хотя говорить о точной дате релиза пока рано, хорошая новость в том, что спецификация JEP 401 остается стабильной на протяжении года, а ее реализация активно продвигается.


Регистрируйтесь на главную конференцию про Spring на русском языке от сообщества Spring АйО! В мероприятии примут участие не только наши эксперты, но и приглашенные лидеры индустрии.

Теги:
Хабы:
Всего голосов 18: ↑18 и ↓0+19
Комментарии8

Публикации

Информация

Сайт
t.me
Дата регистрации
Численность
11–30 человек