【问题标题】:Do Java 8 streams produce slower code than plain imperative loops?Java 8 流产生的代码是否比普通的命令式循环慢?
【发布时间】:2018-01-14 11:00:01
【问题描述】:

关于函数式编程的炒作太多了,尤其是新的 Java 8 流 API。它被宣传为旧的良好循环和命令式范例的良好替代品。 确实,有时它看起来不错并且可以很好地完成工作。但是性能呢?

例如这是一篇很好的文章:Java 8: No more loops 使用循环,您可以通过一次迭代完成所有工作。但是使用新的流 API,您将链接多个循环,这会使其速度变慢(对吗?)。 看看他们的第一个样本。在大多数情况下,循环甚至不会遍历整个数组。但是,要使用新的流 API 进行过滤,您必须循环遍历整个数组以过滤掉所有候选者,然后您将能够获得第一个。

在这篇文章中提到了一些懒惰:

我们首先使用过滤器操作查找所有具有Java标签的文章,然后使用findFirst()操作获取第一次出现。由于流是惰性的并且过滤器返回一个流,因此这种方法只处理元素,直到找到第一个匹配项。

作者的懒惰是什么意思?

我做了简单的测试,它表明旧的良好循环解决方案的工作速度比流式方法快 10 倍。

public void test() {
    List<String> list = Arrays.asList(
            "First string",
            "Second string",
            "Third string",
            "Good string",
            "Another",
            "Best",
            "Super string",
            "Light",
            "Better",
            "For string",
            "Not string",
            "Great",
            "Super change",
            "Very nice",
            "Super cool",
            "Nice",
            "Very good",
            "Not yet string",
            "Let's do the string",
            "First string",
            "Low string",
            "Big bunny",
            "Superstar",
            "Last");

    long start = System.currentTimeMillis();
    for (int i = 0; i < 100000000; i++) {
        getFirstByLoop(list);
    }
    long end = System.currentTimeMillis();

    System.out.println("Loop: " + (end - start));

    start = System.currentTimeMillis();
    for (int i = 0; i < 100000000; i++) {
        getFirstByStream(list);
    }
    end = System.currentTimeMillis();

    System.out.println("Stream: " + (end - start));
}

public String getFirstByLoop(List<String> list) {

    for (String s : list) {
        if (s.endsWith("string")) {
            return s;
        }
    }

    return null;
}

public Optional<String> getFirstByStream(List<String> list) {
    return list.stream().filter(s -> s.endsWith("string")).findFirst();
}

结果是:

循环:517

流:5790

顺便说一句,如果我使用 String[] 而不是 List 差异会更大!几乎是 100 倍!

问题:如果我正在寻找最佳代码性能,是否应该使用旧的循环命令式方法? FP范式只是为了让代码“更简洁易读”而不是为了性能吗?

我是否遗漏了什么,新的流 API 至少可以与循环命令式方法一样高效?

【问题讨论】:

  • 这不是我们对 Java 进行基准测试的方式。但是,是的,Stream 比简单的循环复杂得多——在这种微不足道的情况下它会更慢。
  • 这是一个典型的比一文不值的基准;它比一文不值更糟糕,因为它会产生一个让你认为它有意义的数字。实际上,流通常与传统循环一样快,有时比传统循环快,有时比传统循环慢。而且,如果您正在迭代具有少于数百万个元素的数据集,那么它可能对整体程序性能根本不重要。编写清晰可维护的代码,性能几乎总是足够好。
  • 伙计们,这根本不是基准!这是问题。请告诉我我错了,getFirstByStream 的性能真的比 getFirstByLoop 好,以任何对你信任的方式,我们会完成的。或者请告诉我什么时候流的性能更高,并且应该优先考虑性能。谢谢。
  • @engilyin 这就是 point,您必须针对 given 场景进行测量;然后使用分析器测量您的应用程序并证明流是弱点;最后一点是你应该正确地测量,这并不容易。但是 Brian Goetz 回答您的问题(Oracle/Java 的架构师)对您来说还不够吗?
  • 作者这种懒惰的意思是,你的理解根本上是错误的。流解决方案不会链接多个循环。因此,您对性能的所有假设也是错误的。

标签: performance java-8 functional-programming java-stream


【解决方案1】:

问题:如果我正在寻找最佳代码性能,是否应该使用旧的循环命令式方法?

现在,可能是的。对于大多数测试,各种基准测试似乎表明流比循环慢。虽然速度不是灾难性的慢。

反例:

可以用循环做等价的事情,你不能用只是循环。

但归根结底,性能很复杂,而且流还不是(还)加快代码速度的灵丹妙药。

FP范式是否只是为了让代码“更简洁易读”而不是为了性能?

不完全是。毫无疑问,FP 范式更简洁并且(对于熟悉它的人来说)更具可读性。

但是,通过使用 FP 范式来表达它,以一种可能会以使用循环和赋值表达的代码更难实现的方式进行优化的方式来表达它。 FP 代码也更适合形式化方法;即正确性的正式证明。

(在讨论流的上下文中,“可以优化”是指在未来的 Java 版本中。)

【讨论】:

    【解决方案2】:

    惰性是关于如何从流的源中获取元素 - 即按需。如果需要采取更多的元素 - 他们会,否则他们不会。这是一个例子:

     Arrays.asList(1, 2, 3, 4, 5)
                .stream()
                .peek(x -> System.out.println("before filter : " + x))
                .filter(x -> x > 2)
                .peek(System.out::println)
                .anyMatch(x -> x > 3);
    

    注意每个元素如何通过整个阶段管道;即filter 一次应用于一个元素 - 不是所有元素,因此filter 返回Stream&lt;Integer&gt;。这允许流短路,因为anyMatch 甚至不处理5,因为根本不需要。

    请注意,并非所有中间操作都是惰性的。例如 sorteddistinct 不是 - 这些被称为 stateful 中间操作。以这种方式思考 - 要对您确实需要遍历整个源的元素进行实际排序。另一个不直观的例子是flatMap,但这并不能保证并且看起来更像是一个错误,更多阅读here

    速度取决于你如何测量,用java测量微基准并不容易,事实上的工具是jmh - 你可以试试。这里有很多关于 SO 的帖子表明流确实更慢(这在正常情况下 - 他们有基础设施),但实际上差别并不大。

    【讨论】:

    • 难吗?是的。至少非常使用微基准测试,连续多次运行测试至关重要。 Java 一开始需要“安定下来”,它不仅仅局限于类加载。在大多数基准测试第一次通过后,类加载就解决了,但是我匆忙编写的任何基准测试都记住,前 3 或 4 次运行会产生废话,其他线程可能需要冷静下来等待(),并且如果您正在使用垃圾收集器(IMO,根据定义,不再是微型基准),那么您可能永远无法了解尸体被埋在哪里。
    • @alife 正是我说要使用 JMH
    • “正是我说使用JMH”的原因(?)是的,我知道;我不是说你没有那样说。我同意创建一个微基准很困难,并且提供了如何使一个有点有用,或者至少减轻使它失效的问题。
    • @alife 我明白了。并同意,JMH 会为您处理很多事情,同时也提供很多功能。它已成为 Java 世界中事实上的微基准测试工具。
    猜你喜欢
    • 2016-04-06
    • 2021-04-26
    • 1970-01-01
    • 1970-01-01
    • 2016-04-10
    • 1970-01-01
    • 2020-05-24
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多