【问题标题】:Would Stream.toList() perform better than Collectors.toList()Stream.toList() 会比 Collectors.toList() 表现更好吗
【发布时间】:2021-04-20 19:37:30
【问题描述】:

JDK 正在引入 API Stream.toList()JDK-8180352。这是我尝试将其性能与现有Collectors.toList 进行比较的基准代码:

@BenchmarkMode(Mode.All)
@Fork(1)
@State(Scope.Thread)
@Warmup(iterations = 20, time = 1, batchSize = 10000)
@Measurement(iterations = 20, time = 1, batchSize = 10000)
public class CollectorsVsStreamToList {

    @Benchmark
    public List<Integer> viaCollectors() {
        return IntStream.range(1, 1000).boxed().collect(Collectors.toList());
    }

    @Benchmark
    public List<Integer> viaStream() {
        return IntStream.range(1, 1000).boxed().toList();
    }
}

结果汇总如下:

Benchmark                                                       Mode  Cnt   Score    Error  Units
CollectorsVsStreamToList.viaCollectors                         thrpt   20  17.321 ±  0.583  ops/s
CollectorsVsStreamToList.viaStream                             thrpt   20  23.879 ±  1.682  ops/s
CollectorsVsStreamToList.viaCollectors                          avgt   20   0.057 ±  0.002   s/op
CollectorsVsStreamToList.viaStream                              avgt   20   0.040 ±  0.001   s/op
CollectorsVsStreamToList.viaCollectors                        sample  380   0.054 ±  0.001   s/op
CollectorsVsStreamToList.viaCollectors:viaCollectors·p0.00    sample        0.051            s/op
CollectorsVsStreamToList.viaCollectors:viaCollectors·p0.50    sample        0.054            s/op
CollectorsVsStreamToList.viaCollectors:viaCollectors·p0.90    sample        0.058            s/op
CollectorsVsStreamToList.viaCollectors:viaCollectors·p0.95    sample        0.058            s/op
CollectorsVsStreamToList.viaCollectors:viaCollectors·p0.99    sample        0.062            s/op
CollectorsVsStreamToList.viaCollectors:viaCollectors·p0.999   sample        0.068            s/op
CollectorsVsStreamToList.viaCollectors:viaCollectors·p0.9999  sample        0.068            s/op
CollectorsVsStreamToList.viaCollectors:viaCollectors·p1.00    sample        0.068            s/op
CollectorsVsStreamToList.viaStream                            sample  525   0.039 ±  0.001   s/op
CollectorsVsStreamToList.viaStream:viaStream·p0.00            sample        0.037            s/op
CollectorsVsStreamToList.viaStream:viaStream·p0.50            sample        0.038            s/op
CollectorsVsStreamToList.viaStream:viaStream·p0.90            sample        0.040            s/op
CollectorsVsStreamToList.viaStream:viaStream·p0.95            sample        0.042            s/op
CollectorsVsStreamToList.viaStream:viaStream·p0.99            sample        0.050            s/op
CollectorsVsStreamToList.viaStream:viaStream·p0.999           sample        0.051            s/op
CollectorsVsStreamToList.viaStream:viaStream·p0.9999          sample        0.051            s/op
CollectorsVsStreamToList.viaStream:viaStream·p1.00            sample        0.051            s/op
CollectorsVsStreamToList.viaCollectors                            ss   20   0.060 ±  0.007   s/op
CollectorsVsStreamToList.viaStream                                ss   20   0.043 ±  0.006   s/op

当然,领域专家的第一个问题是基准测试程序是否正确?测试类在 MacOS 上执行。如果需要任何进一步的详细信息,请告诉我。

跟进,据我从读数推断,Stream.toList 的平均时间、吞吐量和采样时间看起来比Collectors.toList 更好。这种理解正确吗?

【问题讨论】:

  • Stream::toList 在某些情况下很可能更有效——但这实际上取决于细节。 Stream::toList 建立在 toArray 的基础上,对于具有 SIZED(最好是 SUBSIZED,用于并行流)特征的源,与 collect 相比,toArray 经过优化以减少重新分配和复制。
  • 但是,如果您尝试测量两个收集器之间的差异,则应尽量减少流管道其余部分的工作。我会将源代码生成和装箱移出基准方法并放入在基准方法之外初始化的@State 变量中,例如Stream.of(data).toList()。拳击肯定会扭曲您的数据。我还包括并行运行。
  • @Holger 默认实现中的额外步骤是在this.toArray 违反其规范并保留对返回数组的引用时强制执行防御性副本。如果没有防御性副本,则可以修改从默认 toList 实现返回的列表。
  • @StuartMarks toArray 是 Stream API 的一部分,因此如果实现违反规范并返回共享数组,toArray 方法的调用者可能已经中断。为什么toList() 方法的调用者要比toArray() 的调用者获得更多的保证?这很简单:如果实现尊重规范,这些方法就会按照规范所说的去做。修复可能损坏的实现不是默认实现的任务。防御性副本适用于像 String 这样的基本类,但不适用于永远不能保证是真正不可变集合的包装器。
  • @StuartMarks 好的,这不是地方。仅最后一条评论,有很多地方,JDK 代码的结果取决于接口的未知实现的正确性,并且在这个地方有这样的不信任/昂贵的防御性编码看起来很随意。而且,在文档中,这绝对是不必要的。

标签: java performance java-stream jmh java-16


【解决方案1】:

Stream::toList 构建在 toArray 之上,而不是 collecttoArray 中有许多优化,使其可能比收集更快,尽管这在很大程度上取决于细节。如果流管道(从源到最终中间操作)是SIZED,则可以预先确定目标数组的大小(而不是像toList 收集器必须做的那样可能重新分配。)如果管道更远SUBSIZED,则并行执行不仅可以预先确定结果数组的大小,还可以计算每个分片的精确偏移量,因此每个子任务都可以将其结果放在正确的位置,从而无需将中间结果复制到最终结果中。

因此,根据细节,toList 可能比collect 快得多。

【讨论】:

  • 即使对于大小不一的流,内部使用的旋转缓冲区也可能比像Collectors.toList() 那样填充ArrayList 更有效。
猜你喜欢
  • 2021-05-04
  • 1970-01-01
  • 2012-08-03
  • 2016-05-20
  • 2015-08-21
  • 2019-06-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多