【问题标题】:What is the most efficient List-Type for Java parallelStream?Java parallelStream 最有效的 List-Type 是什么?
【发布时间】:2015-06-25 00:19:29
【问题描述】:

我有一个List<String> toProcess,我想进一步处理它

toProcess.parallelStream().map(/*some function*/).collect(Collectors.toList());

对于初始列表,从这种多线程中获得最佳速度的最佳列表类型(如 LinkedList、ArrayList 等)是什么?

附加信息: 预期的元素计数范围为 10^3-10^5,但单个元素可能会变得非常大(10^5-10^6 字符)。


另外,我可以在所有地方使用String[],因为字符串的数量保证不会改变(results 将包含与 toProcess 一样多的元素)。


无论哪种方式,我都必须在最后按顺序遍历所有元素。目前我使用foreach-loop 来组装最终结果。这可以很容易地更改为常规的for-loop。

【问题讨论】:

  • OT,但读起来不错:zeroturnaround.com/rebellabs/…
  • @ThomasJunk 幸运的是,我的程序的唯一目的是将方法应用于一堆字符串并将组装的输出写入文件。但无论如何,谢谢,教我小心处理 Java-8 和 WebApplications。
  • 我会选择任何列表并对其进行基准测试,然后尝试其他几种列表,看看是否有什么不同。

标签: java performance list java-8 java-stream


【解决方案1】:

如果您确定输出元素的数量等于输入元素的数量,并且您对作为结果的数组感到满意,那么一定要使用toArray 而不是收集器。如果管道始终具有固定大小,则将使用正确的大小预先分配目标数组,并行操作将其结果直接存入目标数组的正确位置:不进行复制、重新分配或合并。

如果你想要一个List,你总是可以使用Arrays.asList 包装结果,但当然你不能在结果中添加或删除元素。

收藏家

如果上述条件之一不成立,则需要处理收集器,它们有不同的权衡。

收集器以线程受限的方式对中间结果进行操作,从而并行工作。然后将中间结果合并到最终结果中。有两种操作需要考虑:1) 将单个元素累积成中间结果,2) 将中间结果合并(或组合)成最终结果。

LinkedListArrayList 之间,ArrayList 可能更快,但您可能应该对此进行基准测试以确定。请注意,Collectors.toList 默认使用 ArrayList,尽管这可能会在未来的版本中更改。

链表

每个正在累积的元素 (LinkedList.add) 都涉及分配一个新的列表节点并将其挂接到列表的末尾。将节点挂钩到列表非常快,但这涉及到对每个流元素进行分配,随着累积的进行,这可能会导致少量垃圾收集。

合并 (LinkedList.addAll) 也相当昂贵。第一步是将源列表转换为数组;这是通过遍历列表的每个节点并将元素存储到临时数组中来完成的。然后,代码遍历这个临时数组并将每个元素添加到目标列表的末尾。如上所述,这会导致为每个元素分配一个新节点。因此,合并操作非常昂贵,因为它迭代源列表中的每个元素两次,并为每个元素分配内存,这可能会引入垃圾收集开销。

数组列表

每个元素的累积通常涉及将其附加到包含在ArrayList 中的数组的末尾。这通常很快,但如果数组已满,则必须重新分配并复制到更大的数组中。 ArrayList 的增长策略是将新数组分配为比当前数组大 50%,因此重新分配的发生与添加元素数量的对数成正比,这还不错。然而,所有元素都必须被复制,这意味着之前的元素可能需要被复制多次。

合并ArrayList 可能比LinkedList 便宜得多。将ArrayList 转换为数组涉及将源中的元素批量复制(不是一次一个)到临时数组中。必要时调整目标数组的大小(在这种情况下很可能),需要对所有元素进行批量复制。然后将源元素从临时数组批量复制到目标,该目标已预先调整大小以容纳它们。

讨论

鉴于上述情况,ArrayList 似乎会比LinkedList 快。然而,即使收集到ArrayList 也需要一些不必要的重新分配和复制许多元素,可能需要多次。一个潜在的未来优化是Collectors.toList 将元素累积到为快速追加访问而优化的数据结构中,最好是预先调整大小以适应预期元素数量的数据结构。支持快速合并的数据结构也是可能的。

如果您需要做的只是迭代最终结果,那么滚动您自己的具有这些属性的数据结构应该不会太难。如果不需要是完整的列表,则应该可以进行重大简化。它可以累积成预先确定大小的列表以避免重新分配,合并只会将它们简单地收集到树结构或列表列表中。请参阅 JDK 的 SpinedBuffer(私有实现类)以获取想法。

【讨论】:

  • 我认为让Collectors.toList 在内部使用SpinedBuffer 将是一个简单的更改(令人惊讶的是,它在当前版本中没有),但是,预分配具有适当容量的列表非常困难,因为Collector API 只是缺少一种允许Stream 告诉Collector 所需容量的方法……
  • 如果 Java 提供了一种在恒定时间内连接两个这样的列表的内部方法,LinkedList 的故事似乎会好得多。我想知道他们为什么不这样做?
  • @BrandonMintern java.util.concurrent 包有BlockingQueue.drainTo() 似乎具有正确的语义:元素从源中删除并在单个操作中添加到目标。 LinkedList 虽然没有实现 BlockingQueue。研究为快速合并优化的数据结构(例如SpinedBuffer)似乎更有希望。
【解决方案2】:

考虑到上下文切换和一般多线程的成本。在一种列表类型之间切换的性能增益通常真的微不足道。即使您使用次优列表 - 也没关系。

如果您真的在乎,那么ArrayList 因为缓存的局部性可能会做得更好,但这取决于。 p>

【讨论】:

  • 您不情愿地回答“ArrayList”是对的,但您错过了故事中更重要的部分。是的,位置很重要,但这主要是单线程思维;在评估并行算法的可扩展性时,您非常关心递归拆分源代码的效率。 ArrayList 恰好在这里也很出色,所以你的答案是正确的,但主要是因为错误的原因。
  • 我所说的缓存局部性的意思是,将一个数组分成 N 位用于 N 个线程允许将数组保留在所有 N 个线程的缓存中。无论如何,我不情愿的回答是无关紧要的,因为不会忘记缓存的算法看起来并不像这样——它们很少用 Java(或任何其他不允许你指定内存布局的语言)编写,而且它们当然不使用 . parallelStream (顺便说一句,这很棒 - 我喜欢 .prallelStream 的简单并行性)
【解决方案3】:

一般来说,ArrayListLinkedList 相比对并行化更友好,因为数组很容易拆分成块以交给每个线程。

但是,由于您的终端操作是将结果写入文件,因此并行化可能对您毫无帮助,因为您可能会受到 IO 的限制,而不是 CPU 的限制。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-01-28
    • 2020-07-05
    • 1970-01-01
    • 2011-08-17
    • 1970-01-01
    • 1970-01-01
    • 2021-03-14
    • 2011-02-19
    相关资源
    最近更新 更多