用 JMH 检测 Lambdas 序列化性能

时间:2022-07-16 19:41:26

本文将介绍如何进行 Java Lambdas 序列化性能检测、Lambdas 的重要性以及 Lambdas 在分布式系统中的应用。

Lambdas 表达式是 Java 8 中万众期待的新特性,其若干用途包括:

  1. 为匿名内部类减少所需样本代码。

  2. 缩小值的作用域。Lambdas 表达式中的 this 不会涉及到外部类,减少了内存泄露。

  3. 轻松集成现有 API 与新的 Streams API。

Lambdas 另一个鲜有人知的特点就是可被序列化。

为什么对 Lambda 序列化?

序列化有益于对象的状态持久化和网络传输。 Lambdas 应该尽可能无状态,这样就可以保存 Lambdas ,但这并不是典型的用例。

Lambdas 表达式的目的是给程序库传递代码片段,使之与库中正在运行的程序交互。但是如果程序库支持像 Chronicle Engine 这样的分布式系统,那又会怎么样?

什么是 Chronicle Engine?

Chronicle Engine 是一个库,可让用户使用各自的应用程序远程访问数据结构,无论是用 Java、C# 客户端,还是用 NFS 文件系统。该库还支持存储和持久化数据,也支持复制。

分布式系统中的 Lambdas

对于某些局部运行的操作,使用 Lambdas 执行不失为一种简单可行的方法。示例操作如下:

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

这里没有必要知道数据的具体存储位置,如果是远程服务器,就会在那台服务器上对 Lambda 序列化,然后执行,并将结果返回。

用 JMH 检测 Lambdas 序列化性能

上图显示了 OneAPM 如何监控和让 Java 应用之间的调用关系可视化。

Capturing Lambda

不获取字段的 Lambda 可以被 Java 更高效地处理,因为每个实例都一样,所以并不需要每次都创建新的对象。但是,若编译时 Lambda 获取到未知值,就需要创建新的对象,并将获取的值保存。

Non capturing Lambda

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

Capturing Lambda

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

可序列化的 Lambdas

Lambdas 默认是不可序列化的,必须实现一种可序列化的接口。可以使用强制转换把接口类型转换为所需类型的一种方式。

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 + “*”);

Lambdas 的实时查询

利用序列化的 Lambdas,可进行如下所示的实时查询:

// 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.

可查询接口是必需的,因此过滤器 Predicate 和 map 函数也必须隐式可序列化。如果需要使用 Streams API,那就要使用早期较为复杂的数据类型转换函数 cast。

序列化 Lambdas 的性能

笔者曾经在一个字符串中写入符号“*”,并使用 JMH 对简单的序列化的和反序列化的 Lambda 进行时延采样,然后比较采集和非采集两种情况下的时延,发送枚举时两种情况下的时延也一并比较。代码和结果如下表所示:

99.99%的时延意味着该试验的99.99%都是在时延之中。时间都用微秒计算。

Test Typical latency 99.99% latency
Java Serialization, non-capturing 33.9 µs 215 µs
Java Serialization, capturing 36.3 µs 704 µs
Java Serialization, with an enum 7.9 µs 134 µs
Chronicle Wire (Text), non-capturing 20.4 µs 147 µs
Chronicle Wire (Text), capturing 22.5 µs 148 µs
Chronicle Wire (Text), with an enum 1.2 µs 5.9 µs
Chronicle Wire (Binary), non-capturing 11.7 µs 103 µs
Chronicle Wire (Binary), capturing 12.7 µs 135 µs
Chronicle Wire (Binary), with an enum 1.0 µs 1.2 µs

为什么要使用枚举?

使用 Lambda 是很简单,但它效率不高时,就需要找一个备选方案。所以当 Lambda 的使用造成性能问题时,就要使用备选方案。

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

为考察使用枚举所起到的作用,可以比较发送到服务器的数据量的多少,在那里可以看到所有序列化的数据。

下面就是当在 TextWire 中序列化时,非采集的 Lambda 的情况。(基于 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: [
]
}

枚举序列化如下所示:

!Functions APPEND_STAR

注意:当需要采集某些值时,不可以使用枚举。我们要做的就是让你通过传递有附加参数的枚举,以获得最有效的组合。

使用枚举就像存储过程

用枚举代替 Lambdas 的一个好处就是,可以跟踪所有功能客户执行和整合的方式。某些函数使用广泛,运用枚举使得修复任一单独函数中产生的bug更为容易,因此会被经常使用。举一个简单的例子,MapFunction 起初有很多不同的 Lambdas,而现在已经被归为一类。

结论

如果所使用的 API 支持,可将 Lambdas 用于分布式应用程序。如果需要附加的性能,也可以使用枚举。

原文地址:https://dzone.com/articles/measuring-the-serialization-performance-of-lambdas