【问题标题】:Are there any direct or indirect performance benefits of java 8 sequential streams?java 8顺序流有直接或间接的性能优势吗?
【发布时间】:2019-06-11 19:23:43
【问题描述】:

在阅读有关顺序流的文章时,我想到了一个问题,即使用顺序流与传统的 for 循环相比有什么性能优势,或者流只是顺序语法糖,会带来额外的性能开销?

考虑下面的示例,我看不到使用顺序流的任何性能优势:

Stream.of("d2", "a2", "b1", "b3", "c")
    .filter(s -> {
        System.out.println("filter: " + s);
        return s.startsWith("a");
})
    .forEach(s -> System.out.println("forEach: " + s));

使用经典的java:

String[] strings = {"d2", "a2", "b1", "b3", "c"};
        for (String s : strings)
        {
            System.out.println("Before filtering: " + s);
            if (s.startsWith("a"))
            {
                System.out.println("After Filtering: " + s);
            }
        }

点这里是 a2 的流处理仅在 d2 上的所有操作完成后才开始(之前我认为当 d2 由 foreach 处理时,过滤器会在 a2 上进行分层操作,但根据本文并非如此:https://winterbe.com/posts/2014/07/31/java8-stream-tutorial-examples/),经典 java 也是如此,那么除了“富有表现力”和“优雅”编码风格之外,使用流的动机应该是什么?我知道在处理流时编译器会产生性能开销,有人知道吗/在使用顺序流时体验过任何性能优势吗?

【问题讨论】:

    标签: java java-8


    【解决方案1】:

    首先,让特殊情况,例如省略冗余的sorted 操作或返回count() 上的已知大小,除此之外,操作的时间复杂度通常不会改变,因此执行时间上的所有差异都是通常是一个恒定的偏移量或一个(相当小的)因素,而不是根本性的变化。


    您始终可以编写一个手动循环,其与内部的 Stream 实现基本相同。因此,this answer 提到的内部优化总是会因为“但我可以在我的循环中做同样的事情”而被驳回。

    但是……当我们将“流”与“循环”进行比较时,假设所有手动循环都以针对特定用例的最有效方式编写真的合理吗?无论调用代码作者的经验水平如何,特定的 Stream 实现都会将其优化应用于所有适用的用例。我已经看到循环错过了短路或执行特定用例不需要的冗余操作的机会。

    另一方面是执行某些优化所需的信息。 Stream API 是围绕Spliterator 接口构建的,它可以提供源数据的特征,例如它允许查明数据是否具有需要为某些操作保留的有意义的顺序,或者它是否已经预先排序、自然顺序或使用特定的比较器。它还可以在可预测的情况下提供预期的元素数量,作为估计值或精确值。

    接收任意Collection 的方法,用普通循环实现算法,很难找出是否有这样的特征。 List 意味着有意义的顺序,而 Set 通常不会,除非它是 SortedSetLinkedHashSet,而后者是特定的实现类,而不是接口。因此,针对所有已知星座的测试可能仍然会错过具有预定义接口无法表达的特殊合约的 3rd 方实现。

    当然,从 Java 8 开始,您可以自己获取 Spliterator 来检查这些特性,但这会使您的循环解决方案变得不简单,并且还意味着重复使用 Stream API 已经完成的工作。


    在基于 Spliterator 的 Stream 解决方案和传统循环之间还有另一个有趣的区别,即在迭代数组以外的东西时使用 Iterator。该模式是在迭代器上调用hasNext,然后调用next,除非hasNext 返回false。但是Iterator 的合约并没有强制使用这种模式。调用者可以在没有hasNext 的情况下调用next,甚至可以多次调用,当已知它成功时(例如,您已经知道集合的大小)。此外,调用者可以多次调用hasNext 而没有next,以防调用者不记得上一次调用的结果。

    因此,Iterator 实现必须执行冗余操作,例如循环条件被有效地检查了两次,一次在hasNext,返回一个boolean,一次在next,当不满足时抛出一个NoSuchElementException。通常,hasNext 必须执行实际的遍历操作并将结果存储到Iterator 实例中,以确保结果在随后的next 调用之前保持有效。反过来,next 操作必须检查这种遍历是否已经发生,或者它是否必须自己执行操作。在实践中,热点优化器可能会也可能不会消除Iterator 设计带来的开销。

    相比之下,Spliterator 有一个单一的遍历方法boolean tryAdvance(Consumer<? super T> action),它执行实际操作返回是否存在元素。这显着简化了循环逻辑。甚至还有用于非短路操作的void forEachRemaining(Consumer<? super T> action),它允许实际实现提供整个循环逻辑。例如,在ArrayList 的情况下,该操作将在索引上进行简单的计数循环,执行普通数组访问。

    您可以将此类设计与例如BufferedReaderreadLine(),执行操作并在最后一个元素之后返回null,或正则表达式Matcherfind(),执行搜索,更新匹配器的状态并返回成功状态。

    但是,在具有专门用于识别和消除冗余操作的优化器的环境中,很难预测此类设计差异的影响。要点是基于 Stream 的解决方案有可能变得更快,而它是否会在特定场景中实现取决于很多因素。正如开头所说,它通常不会改变整体时间复杂度,这更值得担心。

    【讨论】:

    • 感谢您提供如此详细的解释。现在它有点容易消化了。
    【解决方案2】:

    可能(并且已经有一些技巧)在底层,而传统的 for 循环则没有。例如:

    Arrays.asList(1,2,3)
          .map(x -> x + 1)
          .count();
    

    由于 java-9,map 将被跳过,因为您并不真正关心它。

    或者内部实现可能会检查某个数据结构是否已经排序,例如:

    someSource.stream()
              .sorted()
              ....
    

    如果someSource 已经排序(如TreeSet),在这种情况下sorted 将是空操作。其中有许多优化是在内部完成的,未来可能会做更多的优化。

    【讨论】:

    • 这样的优化也可以通过编程实现:例如 (!(o instanceOf TreeSet)) 然后做... ,但是有没有经典编码风格无法实现的性能优势?
    • @Hitesh 当然是,但还有很多不容易追踪的
    【解决方案3】:

    如果您仍要使用流,您可以使用 Arrays.stream 从数组中创建一个流,并使用 forEach 作为:

    Arrays.stream(strings).forEach(s -> {
        System.out.println("Before filtering: " + s);
        if (s.startsWith("a")) {
            System.out.println("After Filtering: " + s);
        }
    });
    

    在性能说明中,由于您愿意遍历整个数组,因此在循环上使用流并没有特别的好处。有关它的更多信息已在In Java, what are the advantages of streams over loops? 和其他相关问题中进行了讨论。

    【讨论】:

      【解决方案4】:

      enter image description here如果使用stream,我们可以使用parallel(),如下:

      Stream<String> stringStream = Stream.of("d2", "a2", "b1", "b3", "c")
                  .parallel()
                  .filter(s -> s.startsWith("d"));
      

      它更快,因为您的计算机通常能够同时运行多个线程。

      测试一下:

      @Test
      public void forEachVsStreamVsParallelStream_Test() {
          IntStream range = IntStream.range(Integer.MIN_VALUE, Integer.MAX_VALUE);
          StopWatch stopWatch = new StopWatch();
      
          stopWatch.start("for each");
          int forEachResult = 0;
          for (int i = Integer.MIN_VALUE; i < Integer.MAX_VALUE; i++) {
              if (i % 15 == 0)
                  forEachResult++;
          }
          stopWatch.stop();
      
      
          stopWatch.start("stream");
          long streamResult = range
                  .filter(v -> (v % 15 == 0))
                  .count();
          stopWatch.stop();
      
      
          range = IntStream.range(Integer.MIN_VALUE, Integer.MAX_VALUE);
          stopWatch.start("parallel stream");
          long parallelStreamResult = range
                  .parallel()
                  .filter(v -> (v % 15 == 0))
                  .count();
          stopWatch.stop();
      
          System.out.println(String.format("forEachResult: %s%s" +
                          "parallelStreamResult: %s%s" +
                          "streamResult: %s%s",
                  forEachResult, System.lineSeparator(),
                  parallelStreamResult, System.lineSeparator(),
                  streamResult, System.lineSeparator()));
      
          System.out.println("prettyPrint: " + stopWatch.prettyPrint());
          System.out.println("Time Elapsed: " + stopWatch.getTotalTimeSeconds());
      }
      

      【讨论】:

        猜你喜欢
        • 2015-01-06
        • 1970-01-01
        • 2010-09-06
        • 2016-12-08
        • 2010-09-12
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多