【问题标题】:Quickly degrading stream throughput with chained operations?使用链式操作快速降低流吞吐量?
【发布时间】:2018-10-04 12:08:28
【问题描述】:

我希望像limit() 这样的简单中间流操作的开销很小。但这些示例之间的吞吐量差异实际上是显着的:

final long MAX = 5_000_000_000L;

LongStream.rangeClosed(0, MAX)
          .count();
// throughput: 1.7 bn values/second


LongStream.rangeClosed(0, MAX)
          .limit(MAX)
          .count();
// throughput: 780m values/second

LongStream.rangeClosed(0, MAX)
          .limit(MAX)
          .limit(MAX)
          .count();
// throughput: 130m values/second

LongStream.rangeClosed(0, MAX)
          .limit(MAX)
          .limit(MAX)
          .limit(MAX)
          .count();
// throughput: 65m values/second

我很好奇:吞吐量快速下降的原因是什么?它与链式流操作或我的测试设置是否一致? (我目前没有用过JMH,只是用秒表设置了一个快速实验)

【问题讨论】:

    标签: java java-stream throughput


    【解决方案1】:

    limit 将导致由流组成一个 slice,并带有一个 split iterator(用于并行操作)。一言以蔽之:低效。此处无操作的开销很大。而且连续两次limit 调用导致两个切片是一种耻辱。

    你应该看看IntStream.limit的实现。

    由于 Streams 仍然相对较新,因此优化应该放在最后;当生产代码存在时。限制3次似乎有点牵强。

    【讨论】:

    • 所以这只是次优实现的问题......?有什么解决方法吗?我们不得不承认开销令人印象深刻(就像limit (x3) 是牵强或人为的一样令人印象深刻)
    • 链式流式表达往往不需要太多开销就可以表达,所以这是一个智能数据结构和算法的问题。一旦你看到更复杂的东西,例如分组,可能会有更简单的数据结构来避免这种结构处理。所以no,没有特效药。
    • @JoopEggen 唯一的药是在 Stream 实现本身,我猜
    • @Eugene 我已经看到了带有分组/临时列表/flatMap 的流代码,这也可以通过一个更简单、更直接的 Stream sn-p 使用对变量的引用(AtomicInteger)来完成。分组本身很好,也不错,但 Streams 不像 SQL 或函数式语言那样具有声明性,在这些语言中可能会发生强大的优化。
    • @JoopEggen 这不是 API 设计的问题。我们正在使用另一种实现(与 Java 7 兼容的反向端口),它根本不会遇到这些问题。在那里,直接在rangeClosed 之后应用limit 与您首先使用适应范围的效果相同。同样,多个后续的limit 操作就像您只用最小的数字执行了一个limit 一样。但是,我仍然没有感觉到这种差异在实践中具有很大的相关性。更重要的是,避免这些过长的调用链(避免内联限制)
    【解决方案2】:

    这是 Stream API 中的一个未实现的实现(不知道如何调用它)。

    在第一个示例中,您知道 count 没有实际计数 - 没有可能清除名为 SIZED 的内部标志的 filter(例如)操作。如果你改变它并检查它实际上有点有趣:

    System.out.println(
                LongStream.rangeClosed(0, Long.MAX_VALUE)
                        .spliterator()
                        .hasCharacteristics(Spliterator.SIZED)); // reports false
    
    System.out.println(
                LongStream.rangeClosed(0, Long.MAX_VALUE - 1) // -1 here
                        .spliterator()
                        .hasCharacteristics(Spliterator.SIZED)); // reports true
    

    limit - 即使没有基本 (AFAIK) 限制,也不会引入 SIZED 标志:

    System.out.println(LongStream.rangeClosed(0, MAX)
                .limit(MAX)
                .spliterator()
                .hasCharacteristics(Spliterator.SIZED)); // reports false
    

    既然你到处都算数,Stream API 内部不知道流是否为SIZED,它只是算数;而如果流是SIZED - 报告计数会很好,即时。

    当您添加limit 几次时,您只会变得更糟,因为它必须每次都限制这些限制。

    以 java-9 为例,情况有所改进:

    System.out.println(LongStream.rangeClosed(0, MAX)
                .map(x -> {
                    System.out.println(x);
                    return x;
                })
                .count());
    

    在这种情况下,map 根本不计算,因为不需要它 - 没有中间操作会更改流的大小。

    理论上,Stream API 可能会看到您是 limiting 并且 1) 引入 SIZED 标志 2) 看到您有多次调用 limit 并且可能只使用最后一个。 目前这个还没有做,但是这个范围很有限,有多少人会这样滥用limit呢?所以不要指望这部分很快会有任何改进。

    【讨论】:

    • 一个有趣的细节,很高兴知道后来的 Java 版本对事情进行了优化。但真正的原因应该在其他地方,因为limit()(至少在理论上)是一个非常便宜的操作,调用它 3 次不会导致性能下降 1-2 个数量级。
    • @Matthias 为什么你认为这将是一个非常便宜的操作?据我从实现中可以看出,它每次都会检查limit,然后再进一步处理该元素。那将是很多检查;但它也很可能是你测试这个的方式
    • 因为Documentation 暗示了这个方向,而且我让自己被吞吐量的线性下降所欺骗(并且还希望 CPU 优化能够在期间启动极其可预测的执行模式循环/迭代)。在使用基于迭代器的专用限制进行简短测试后,我看到了与流类似的结果。所以我最初的假设显然是不现实的。
    • @Matthias 如果你真的展示你的测试会很酷,我假设是基于 JMH 的?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2023-02-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-08-22
    • 1970-01-01
    • 2018-07-23
    相关资源
    最近更新 更多