Статьи

Измерение производительности сериализации Lambdas с JMH

Долгожданным дополнением к Java 8 стала Lambdas. Они имеют ряд применений, таких как:

Сокращение стандартного кода, который вам необходим для анонимных внутренних классов.

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

Простая интеграция с существующими API и новым API Streams.

Менее известной особенностью лямбд является то, что они могут быть сериализованы.

Зачем сериализовать лямбду?

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

Лямбды предназначены для передачи фрагментов кода в библиотеку, чтобы обеспечить взаимодействие с тем, что делает библиотека. Но что, если библиотека поддерживает распределенную систему, такую ​​как Chronicle Engine?

Что такое хроника двигателя?

Chronicle Engine — это библиотека, которая позволяет вам получать удаленный доступ к структурам данных в вашем приложении как клиенту Java или C #, так и файловой системе NFS. Он также поддерживает хранение и сохранение данных из кучи, а также репликацию.

Лямбды в распределенных системах

Лямбда может быть простым способом выполнения операции, которая может выполняться или не выполняться локально. Вы можете выполнять такие операции, как:

MapView<String, Long> map = acquireMap(“map-name”, 
                                      String.class, Long.class);
map.put(“key”, 1);
long ret = map.applyToKey(“key”, v -> v + 1); // ret == 2

Мне не нужно знать, где хранятся данные. Если он находится на удаленном сервере, лямбда сериализуется и выполняется на этом сервере, а результат возвращается мне.

На приведенном выше снимке экрана показано, как AppDynamics может отслеживать и визуализировать все ваши Java-приложения.

Захват лямбды

Лямбда, которая не захватывает поля, обрабатывается Java более эффективно. Не нужно каждый раз создавать новый объект, так как все экземпляры будут одинаковыми. Тем не менее, для лямбды, которая захватывает значение, которое не известно во время компиляции, может быть создан новый объект, и он будет содержать захваченное значение.

Не захватывая лямбда

Function<String, String> appendStar = s -> s + "*"

Захват лямбды

String star = "*";
Function<String, String> appendStar = s -> s + star;

Сериализуемые лямбды

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

Function<String, String> appendStar = 
     (Function<String, String> & Serializable) (s -> s + star);

Мне не нравится делать это, потому что это скорее сводит на нет цель сокращения кода котельной плиты. Способ обойти это — определить свой собственный интерфейс, который можно сериализировать.

@FunctionalInterface
public interface SerializableFunction<I, O> 
       extends Function<I, O>, Serializable {

Это позволяет вам написать:

SerializableFunction<String, String> appendStar = s -> s + star;

Или если у вас есть такой метод:

<R> R applyToKey(K key, @NotNull SerializableFunction<E, R> function) {

Звонящий из вашей библиотеки может написать:

String s = map.applyToKey(“key”, s-> s + “*”);

без какого-либо стандартного кода.

Живые Запросы с Лямбдами

Имея сериализуемые лямбда-выражения, вы можете использовать такие запросы, как этот.

// print the last name of all the people in NYC
acquireMap(“people”, String.class, Person.class).query()
  .filter(p -> p.getCity().equals(“NYC”)) // executed on the server
  .map(p → p.getLastName())  // executed on the server
  .subscribe(System.out::println); // executed on the client.

Интерфейс Queryable необходим для того, чтобы фильтр Predicate и функция map были неявно сериализуемыми. Если вы использовали API-интерфейс Streams, вам пришлось бы использовать сложное приведение, использованное ранее.

Производительность сериализации лямбд.

Я использовал JMH для определения задержки как сериализации, так и десериализации простой лямбды, чтобы добавить «*» в строку. Я сравнил не захватывание и захват, а также видел, как это сравнивается с отправкой enum на одно и то же. Код и результаты здесь:

Задержка 99,99% означает, что 99,99% тестов были ниже этой задержки. Время в микросекундах:

Тест

Типичная задержка

Задержка 99,99%

Сериализация Java, без захвата

 33,9 мкс

215 мкс

Сериализация Java, захват

 36,3 мкс

704 мкс

Сериализация Java, с перечислением

7,9 мкс

134 мкс

Хроника проволоки (текст), без захвата

20,4 мкс

147 мкс

Хроника Провода (Текст), захват

22,5 мкс

148 мкс

Хроника Провода (Текст), с перечислением

1,2 мкс

5,9 мкс

Провод Хроники (Бинарный), без захвата

11,7 мкс

103 мкс

Хроника проволоки (двоичная), захват

12,7 мкс

135 мкс

Chronicle Wire (Binary), с перечислением

1,0 мкс

1,2 мкс

Что значит использовать перечисление?

Хотя использование лямбды является простым, это не так эффективно, поэтому вам нужна альтернатива, если окажется, что использование лямбды вызывает проблемы с производительностью.

enum Functions implements SerializableFunction<String, String> {
    APPEND_STAR {
        @Override
        public String apply(String s) {
            return s + '*';
        }
    }
}

Чтобы увидеть, что с помощью enum имеет значение, вы можете сравнить, сколько данных нужно отправить на сервер. Вы можете увидеть все сериализованные данные здесь.

Вот как выглядит не захватывающая лямбда при сериализации в TextWire (на основе YAML)

!SerializedLambda {
  cc: !type lambda.LambdaSerialization,
  fic: net/openhft/chronicle/core/util/SerializableFunction,
  fimn: apply,
  fims: (Ljava/lang/Object;)Ljava/lang/Object;,
  imk: 6,
  ic: lambda/LambdaSerialization,
  imn: lambda$textWireSerialization$ea1ad110$1,
  ims: (Ljava/lang/String;)Ljava/lang/String;,
  imt: (Ljava/lang/String;)Ljava/lang/String;,
  ca: [
  ]
}

и сериализация enum выглядит следующим образом

!Functions APPEND_STAR

Примечание: вы не можете использовать enum, если вам нужно захватить некоторые значения. То, что мы делаем, это позволяет вам передать перечисление с дополнительными аргументами, чтобы получить наиболее эффективную комбинацию.

Использование перечислений как хранимых процедур

Одно из преимуществ использования enum вместо lambdas заключается в том, что вы можете отслеживать все функции, выполняемые клиентами, и консолидироваться. Это облегчает исправление ошибок в любой отдельной функции, которая может использоваться во многих местах. По этой причине мы используем перечисления в некоторых местах. Примером является MapFunction, в которой изначально было много разных лямбд, которые теперь были сгруппированы в один класс. Примечание: использование перечислений не так чисто для реализации.

Вывод

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