Как стать автором
Поиск
Написать публикацию
Обновить
110.9

Просто будь ленивым

Уровень сложностиПростой
Время на прочтение6 мин
Количество просмотров956
Автор оригинала: Per-Ake Minborg

В новом переводе от команды Spring АйО рассматривается JEP 502, который вносит в Java 25 StableValue. StableValue позволяет легко и эффективно реализовывать производительные ленивые конструкции. Новое API позволяет компилятору JIT заранее сворачивать вычисления (constant folding), повышая производительность. Искали Lazy, но нашли золото: StableValue вычисляется один раз и может быть подготовлен Ahead Of Time.


JEP 502: Stable Values (preview) наконец-то принесёт в Java полноценные ленивые конструкции. С этим нововведением мы получаем ленивые значения, функции и коллекции! Преимущество в том, что компилятор HotSpot JIT сможет выполнять свёртку констант (constant-folding) для всех вариаций объектов, которые оборачиваются ссылкой StableValue. Для этого ссылка типа StableValue должна быть константой (помечена модификаторами  static final), либо если ссылка на StableValue считается транзитивно надёжной (об этом ниже).

Быстрый старт

В следующем примере кода показано, как реализовать высокопроизводительный ленивый логгер с использованием stable значений в JDK 25:

public record OrderControllerImpl(Supplier<Logger> logger)

        implements OrderController {

    @Override
    public void submitOrder(User user, List<Product> products) {
        logger.get().info("order started");
        // ...
        logger.get().info("order submitted");
    }

    /** {@return a new OrderController} */
    public static OrderController create() {

        return new OrderControllerImpl(
                StableValue.supplier(
                        () -> Logger.create(OrderControllerImpl.class)));
    }
}

private static final OrderControler ORDER_CONTROLLER = OrderControllerImpl.create();

ORDER_CONTROLLER.submitOrder(...);
Комментарий от эксперта Spring АйО, Павла Кислова

Никто не страхует ваш код от того, что будет вызван конструктор record. В java нет возможности скрыть canonical constuctor в record, как в scala или переименовать его, как это делают в Dart. Поэтому - это договоренность "пользуемся методом create() вместо вызова конструктора". 

Важно отметить, что record вместо обычного класса здесь используется по причине того, что внутренние поля у record final по-умолчанию. JVM без проблем применяет к ним оптимизацию свертывания констант. Возможность использовать обычный класс с приватным конструктором как это делается для builder-pattern тоже присутствует. Здесь демонстрируется лишь один из возможных вариантов.

Здесь мы используем небольшой трюк, чтобы заставить JIT-компилятор поверить, что поле logger никогда не изменится, — через использование record. Компоненты record-классов очень сложно изменить, и благодаря этому JIT-компилятор доверяет им.

Комментарий от эксперта Spring АйО, Михаила Поливахи

Код выше позволяет вам получить immutable logger, который создается лениво, и который при этом JIT может спокойно свертывать как final поле (В Java уже долгие годы есть аннотация @Stable, которая не доступна обычным разработчикам. Она имеет прямое отношение к этому делу, почитайте)

У опытного инженера наверняка возникает вопрос, чем этот пример отличается от использования logger напрямую, да и вообще зачем так усложнять жизнь? Ведь можно просто написать:

public record OrderControllerImpl(Logger logger) implements OrderController {

@Override
public void submitOrder(User user, List<Product> products) {
logger.info("order started");
// ...
logger.info("order submitted");
}

/** {@return a new OrderController} */
public static OrderController create() {
return new OrderControllerImpl(Logger.create(OrderControllerImpl.class));
}
}

Действительно, в таком случае тоже нет накладных расходов на различного рода виртуальные вызовы через абстракции, а идет прямое обращение к Logger. Но проблема в том, что сам объект logger в heap создается eagerly. Иногда это недопустимо. Иногда объект может быть довольно тяжелый, и не факт, что вообще он понадобиться. И его имеет смысл инициализировать лениво. И StableValue это по сути ваш контракт с JIT компилятором, где Вы ему говорите - дружище, смотри, я тебе гарантирую, что та ссылка, которую продьюсирует StableValue - она не измениться (на самом деле сам дизайн StableValue не даст вам её поменять). Её можно констант фолдить спокойно, можешь мне верить, точно так же, как ты делала с @Stable и внутренними полями в JVM. И вы получаете по сути ленивые финальные поля. Вы создаете immutable объект, но инициализируете его лениво, при этом не теряете в производительности. Вот в чём суть.

Вот ещё один пример кеширования значений без вытеснения (non-evicting), с использованием API stable значений:

private static final Function<Integer, Double> SQRT_CACHE =
        StableValue.function(ffff, i -> StrictMath.sqrt(i));
...

// Вычисляется лениво, но всё ещё может быть свёрнуто JIT-компилятором -> 4
double sqrt16 = SQRT_CACHE.apply(16);

Значения в SQRT_CACHE изначально не установлены. Как только значение, например 4, будет запрошено, соответствующее значение sqrt() будет вычислено и добавлено в SQRT_CACHE. И самое главное - когда JIT впоследствии скомпилирует участок кода выше, то он сможет выполнить свёртку для виртуального вызова sqrt(16) и схлопнуть его на простую константу - 4. 

Комментарий от эксперта Spring АйО, Михаила Поливахи

С простой самописной Lazy оберткой вы бы не смогли просигнализировать JIT-у тот факт, что результат sqrt() неизменяемый. Даже если бы результат был бы final полем, к сожалению, это ни о чём не говорит. Если будет интересно, мы в будущем выпустим статью о том, почему final поля на самом деле не совсем являются final.

Почему это не назвали Lazy?

Это, пожалуй, самый частый вопрос, который мы получали от сообщества. Свойства «ленивого» (lazy) поля — это подмножество свойств «стабильного» (stable) поля. Оба типа предполагают ленивое вычисление в более широком смысле. Тем не менее, стабильное поле гарантированно вычисляется не более одного раза и теоретически может быть вычислено ahead-of-time (например, во время предыдущего прогона приложения).

Если же вы хотите создать свою обертку вокруг StableValue и назвать её, как вам кажется, более выразительно, например Lazy, то сделать это очень просто:

@FunctionalInterface
interface Lazy<T> extends Supplier<T> {
    static <T> Lazy<T> of(Supplier<? extends T> original) {
        return StableValue.supplier(original)::get;
    }
}

private static final Lazy<String> httpResponse = Lazy.of(() -> retrieveHttp(...));
System.out.println("The response was " + httpResponse.get());

Ссылки на методы (как StableValue.supplier(original)::get выше) реализуются с помощью скрытых (hidden) классов. Им также доверяет JIT-компилятор. Поэтому данный класс обеспечит те же оптимизации свёртки констант, что и StableValue.supplier(), и в остальном будет иметь почти такие же характеристики производительности.

Комментарий от эксперта Spring АйО, Михаила Поливахи

Речь про JEP 371. Hidden классы генерирует сама JVM в разных случаях, как вот например в случае с method reference в StableValue.supplier()::get, где VM сгенерирует экземпляр hidden класса, который реализует нужный Lazy/Supplier интерфейс.

JIT компилятор считает и тоже trusted source-ом по разному роду причин, можете почитать JEP для детального разбора.

Что дальше?

Скачайте JDK 25 и посмотрите, сколько производительности и простоты может принести API стабильных значений в ваши текущие приложения.

Чтобы использовать API Stable Value в JDK 25, не забудьте включить preview-функции:

  • Скомпилируйте программу с помощью:
    javac --release 25 --enable-preview Main.java

  • Запустите её с:
    java --enable-preview Main

  • Или запустите программу одной командой через лаунчер исходного кода:
    java --enable-preview Main.java

  • В случае использования jshell, запустите его с ключом:
    jshell --enable-preview

Подробнее о final-полях

Когда сообщество JDK обеспечит доверие и к final-полям экземпляров, можно будет использовать любые классы для хранения stable значений, функций и коллекций — при этом всё ещё достигая свёртки констант. Первый шаг в этом направлении уже сделан.


Присоединяйтесь к русскоязычному сообществу разработчиков на Spring Boot в телеграм — Spring АйО, чтобы быть в курсе последних новостей из мира разработки на Spring Boot и всего, что с ним связано.

Теги:
Хабы:
+1
Комментарии1

Публикации

Информация

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