Java parallelStream 最有效的列表类型是什么?

What is the most efficient List-Type for Java parallelStream?

我有一个 List<String> toProcess 我想用

进一步处理
toProcess.parallelStream().map(/*some function*/).collect(Collectors.toList());

哪种列表类型(如 LinkedList、ArrayList 等)是初始列表从多线程中获得最佳速度的最佳选择?

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


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


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

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

如果您 真的 关心,那么 ArrayList 因为缓存局部性 可能 会做得更好工作,但这取决于。

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

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

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

如果您想要 List,您始终可以使用 Arrays.asList 包装结果,但当然您不能向结果添加或删除元素。

收藏家

如果上述条件之一不成立,那么您需要与收集器打交道,它们有不同的权衡。

收集器通过以 thread-confined 方式对中间结果进行操作来并行工作。然后将中间结果合并到最终结果中。有两个操作需要考虑:1) 将单个元素累加到中间结果中,以及 2) 将中间结果合并(或合并)为最终结果。

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

链表

累积的每个元素 (LinkedList.add) 涉及分配一个新的列表节点并将其挂接到列表的末尾。将节点挂接到列表非常快,但这涉及到为每个流元素分配,随着累积的进行,这可能会产生少量垃圾 collections。

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

数组列表

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

合并 ArrayList 可能比 LinkedList 便宜得多。将 ArrayList 转换为数组涉及将源中的元素批量复制(而不是 one-at-a-time)到临时数组中。必要时调整目标数组的大小(在这种情况下很可能),需要所有元素的批量复制。源元素然后 bulk-copied 从临时数组到目标,已经 pre-sized 容纳它们。

讨论

鉴于以上情况,ArrayList 似乎比 LinkedList 快。但是,即使 collection 到 ArrayList 也需要对许多元素进行一些不必要的重新分配和复制,可能需要多次。一个潜在的未来优化是 Collectors.toList 将元素累积到一个数据结构中,该数据结构针对 fast-append 访问进行了优化,最好是已经 pre-sized 以容纳预期元素数量的数据结构。支持快速合并的数据结构也是可能的。

如果您需要做的只是迭代最终结果,那么滚动您自己的具有这些属性的数据结构应该不会太困难。如果不需要 full-blown 列表,应该可以进行显着简化。它可以累积到 pre-sized 列表中以避免重新分配,合并只会将它们聚集到树结构或 list-of-lists 中。有关想法,请参阅 JDK 的 SpinedBuffer(私有实现 class)。