Java 8 中可序列化 lambda 的性能

Performance of serializable lambdas in Java 8

我在 Brian Goetz 的一些评论中读到了可序列化的 lambda "have significantly higher performance costs compared to nonserializable lambdas"。

我现在很好奇:开销到底在哪里,是什么原因造成的?它只影响 lambda 的实例化,还是影响调用?

在下面的代码中,两种情况( callExistingInstance() 和 callWithNewInstance() )是否会受到 "MyFunction" 的可序列化性的影响,还是仅受到第二种情况的影响?

interface MyFunction<IN, OUT> {
    OUT call(IN arg);
}

void callExistingInstance() {

    long toAdd = 1;
    long value = 0;

    final MyFunction<Long, Long> adder = (number) -> number + toAdd;

    for (int i = 0; i < LARGE_NUMBER; i++) {
        value = adder.call(value);
    }
}

void callWithNewInstance() {

    long value = 0;

    for (int i = 0; i < LARGE_NUMBER; i++) {
        long toAdd = 1;

        MyFunction<Long, Long> adder = (number) -> number + toAdd;

        value = adder.call(value);
    }
}

当您 serialize/deserialize 和实例化时,性能会受到影响。只有你的第二个例子受到打击。它昂贵的原因是当你反序列化时,你的 lambda 的底层 class 是通过一种特殊的反射(具有 create/define 和 class 的能力)而不是普通的旧序列化对象(class 定义从何而来?),以及执行一些安全检查...

通常,lambda 实现的运行时部分会生成一个 class,它基本上由一个实现方法组成。生成这样的 class 所需的信息由运行时对 LambdaMetafactory.metafactory 的 bootstrap 方法调用提供。

启用序列化后,事情变得更加复杂。首先,编译后的代码将使用替代的 bootstrap 方法 LambdaMetafactory.altMetafactory,它提供了更大的灵活性,但必须根据参数数组中指定的标志解析可变参数。

然后生成的 lambda class 必须有一个 writeReplace 方法(参见 Serializable documentation) which has to create and return a SerializedLambda 实例的后半部分,其中包含重新创建 lambda 实例所需的所有信息。自lambda 的 class 的单一实现方法仅包含一个简单的委托调用,该 writeReplace 方法和相关常量信息将乘以生成的 class 大小。

还值得注意的是,您的 class 创建可序列化的 lambda 实例将有一个合成方法 $deserializeLambda$(比较 class documentation of SerializedLambda 作为 lambda writeReplace。这将增加您的 classes 磁盘使用和加载时间(但不影响 lambda 表达式的计算)。


在您的示例代码中,这两种方法都会受到相同时间的影响,因为 bootstrapping 和 class 生成每个 lambda 表达式只发生一次。在随后的评估中,the class generated on the first evaluation will be re-used and only a new instance created (if not even the instance is re-used)。我们在这里讨论的是一次性开销,即使 lambda 表达式包含在循环中,它也只影响第一次迭代。

请注意,如果您在循环中有一个 lambda 表达式,那么 可能 是一个新的 实例 为每次迭代创建它循环外 肯定 在整个循环中只有一个实例。但是这种行为并不依赖于目标接口是否为 Serializable 的问题。它仅取决于表达式是否捕获值(与 this answer 相比)。

请注意,如果您写了

final long toAdd = 1;
MyFunction<Long, Long> adder = (number) -> number + toAdd;

在你的第二种方法中(注意显式 final 修饰符)值 toAdd 将是一个编译时常量并且表达式被编译就像你写了 (number) -> number + 1,即不再捕获值。然后您将在每个循环迭代中获得相同的 lambda 实例(使用当前版本的 Oracle JVM)。因此,是否创建新实例的问题有时取决于上下文的一小部分。但通常情况下,性能影响很小。