我有一个List<String> toProcess
,我想用
toProcess.parallelStream().map(/*some function*/).collect(Collectors.toList());
哪个是最佳列表类型(如LinkedList,ArrayList等),以便从多线程中获得最佳速度的初始列表?
附加信息:预期的元素数量范围大小为10 ^ 3-10 ^ 5,但单个元素可以变得相当大(10 ^ 5-10 ^ 6个字符)。
另外我可以在整个地方使用String[]
,因为字符串数量保证不会改变(结果将包含与 toProcess 一样多的元素)。
无论哪种方式,我必须按顺序迭代所有元素。目前我使用foreach
- 循环来汇总最终结果。这可以很容易地更改为常规for
- 循环。
答案 0 :(得分:5)
如果您确定输出元素的数量等于输入元素的数量,并且您对数组满意,那么肯定使用toArray
而不是收集器。如果管道具有固定的大小,则目标阵列将预先分配正确的大小,并行操作将其结果直接存入正确位置的目标阵列:无复制,重新分配或合并。
如果您想要List
,则可以始终使用Arrays.asList
包装结果,但当然您无法在结果中添加或删除元素。
<强>收藏家强>
如果上述条件之一不成立,那么您需要处理具有不同权衡的收藏家。
收集器通过以线程限制的方式操作中间结果来并行工作。然后将中间结果合并到最终结果中。有两种操作需要考虑:1)将各个元素累积到中间结果中,以及2)将中间结果合并(或组合)成最终结果。
在LinkedList
和ArrayList
之间,ArrayList
可能更快,但您应该对此进行基准确认。请注意,Collectors.toList
默认使用ArrayList
,但在将来的版本中可能会更改。
<强>链表强>
正在累积的每个元素(LinkedList.add
)涉及分配新的列表节点并将其挂钩到列表的末尾。将节点挂钩到列表的速度非常快,但这涉及到每个流元素的分配,这可能会随着累积的进行而产生较小的垃圾收集。
合并(LinkedList.addAll
)也非常昂贵。第一步是将源列表转换为数组;这是通过循环遍历列表的每个节点并将元素存储到临时数组中来完成的。然后,代码遍历此临时数组,并将每个元素添加到目标列表的末尾。如上所述,这导致为每个元素分配新节点。因此合并操作非常昂贵,因为它遍历源列表中的每个元素两次并导致每个元素的分配,这可能会引入垃圾收集开销。
<强>的ArrayList 强>
每个元素的累积通常涉及将其附加到ArrayList
中包含的数组的末尾。这通常非常快,但如果数组已满,则必须重新分配并复制到更大的数组中。 ArrayList
的增长策略是将新数组分配为比当前数组大50%,因此重新分配与添加的元素数量的对数成比例,这不是太糟糕。但是,必须复制所有元素,这意味着可能需要多次复制早期元素。
合并ArrayList
可能比LinkedList
便宜得多。将ArrayList
转换为数组涉及从源到临时数组的元素的批量复制(不是一次一个)。必要时调整目标数组的大小(在这种情况下很可能),需要所有元素的批量副本。然后将源元素从临时数组批量复制到目标,该目标已预先调整大小以容纳它们。
<强>讨论强>
鉴于上述情况,似乎ArrayList
将比LinkedList
更快。但是,即使收集到ArrayList
也需要一些不必要的重新分配和复制许多元素,可能需要多次。潜在的未来优化将是Collectors.toList
将元素累积到为快速附加访问而优化的数据结构中,优选地是预先调整大小以容纳预期数量的元素的数据结构。支持快速合并的数据结构也是可能的。
如果你需要做的就是迭代最终结果,那么滚动你自己的具有这些属性的数据结构就不会太困难了。如果它不需要是一个完整的列表,那么应该可以进行重大的简化。它可以累积到预先调整大小的列表中以避免重新分配,并且合并将简单地将它们收集到树结构或列表列表中。有关创意,请参阅JDK的SpinedBuffer(私人实施类)。
答案 1 :(得分:4)
考虑到上下文切换和一般多线程的成本。在一种列表之间切换的性能提升通常真的无关紧要。即使您使用次优列表 - 也无关紧要。
如果您非常关心,那么ArrayList
因为缓存位置而可能做得更好,但这取决于。
答案 2 :(得分:1)
通常,与ArrayList
相比,LinkedList
对并行化更友好,因为数组很容易分割成片段以交给每个线程。
但是,由于终端操作是将结果写入文件,因此并行化可能对您没有帮助,因为您可能会受到IO的限制,而不受CPU的限制。