我正在重写一个涉及使用 Java 8 处理 1000 万个对象的应用程序,我注意到流可以使应用程序的速度减慢 25%。有趣的是,当我的集合为空时也会发生这种情况,因此这是流的恒定初始化时间。要重现该问题,请考虑以下代码:

    long start = System.nanoTime();
    for (int i = 0; i < 10_000_000; i++) {
        Set<String> set = Collections.emptySet();
        set.stream().forEach(s -> System.out.println(s));
    }
    long end = System.nanoTime();
    System.out.println((end - start)/1000_000);

    start = System.nanoTime();
    for (int i = 0; i < 10_000_000; i++) {
        Set<String> set = Collections.emptySet();
        for (String s : set) {
            System.out.println(s);
        }
    }
    end = System.nanoTime();
    System.out.println((end - start)/1000_000);

结果如下:224 对 5 毫秒。

如果我直接在现场使用 forEach ,即 set.forEach() ,结果将是:12 vs 5ms。

最后,如果我在外面创建一次闭包
Consumer<? super String> consumer = s -> System.out.println(s);

并使用 set.forEach(c) 结果将是 7 对 5 毫秒。

当然,数字很小,我的基准测试非常原始,但是这个例子是否表明初始化流和闭包有开销?

(实际上,由于 set 是空的,在这种情况下,闭包的初始化成本应该不重要,但是,我应该考虑事先创建闭包而不是即时创建闭包)

最佳答案

您在此处看到的成本根本与“闭包”无关,而是与 Stream 初始化的成本有关。

让我们拿你的三个示例代码:

for (int i = 0; i < 10_000_000; i++) {
    Set<String> set = Collections.emptySet();
    set.stream().forEach(s -> System.out.println(s));
}

这会在每个循环中创建一个新的 Stream 实例;至少对于前 10k 次迭代,请参见下文。在 10k 次迭代之后,好吧,JIT 可能足够聪明,可以看出它无论如何都是空操作。
for (int i = 0; i < 10_000_000; i++) {
    Set<String> set = Collections.emptySet();
    for (String s : set) {
        System.out.println(s);
    }
}

这里 JIT 又开始了:空集?好吧,这是一个无操作的故事结束。
set.forEach(System.out::println);

为集合创建了一个 Iterator,它总是空的?同样的故事,JIT 开始了。

你的代码一开始的问题是你没有考虑到 JIT;对于实际测量,在测量之前至少运行 10k 次循环,因为 10k 次执行是 JIT 启动所需的(至少,HotSpot 是这样操作的)。

现在,lambdas:它们是调用站点,它们只链接一次;但当然,初始链接的成本仍然存在,并且在您的循环中,您包括此成本。在进行测量之前尝试只运行一个循环,这样就不会产生这种成本。

总而言之,这不是一个有效的微基准测试。使用 caliper 或 jmh 来真正衡量性能。

一个很好的视频,用于了解 lambdas 如何工作 here 。现在它有点老了,JVM 比使用 lambdas 时要好得多。

如果您想了解更多信息,请查找有关 invokedynamic 的文献。

关于java - Java 8 中流和闭包的成本,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/27715295/

10-13 09:47