[英]Sorting a List in parallel without creating a temporary array in Java 8
Java 8 提供了java.util.Arrays.parallelSort
,它使用 fork-join 框架对数组进行并行排序。 但是没有相应的Collections.parallelSort
用于排序列表。
我可以使用toArray
,对该数组进行排序,并将结果存储回我的列表中,但这会暂时增加内存使用量,如果我使用并行排序,内存使用量已经很高,因为并行排序只会对巨大的列表产生回报。 而不是两倍的内存(列表加上 parallelSort 的工作内存),我使用了三次(列表、临时数组和 parallelSort 的工作内存)。 (Arrays.parallelSort 文档说“该算法需要一个不大于原始数组大小的工作空间”。)
撇开内存使用不谈,Collections.parallelSort 对于看似相当常见的操作也会更方便。 (我倾向于不直接使用数组,所以我肯定会比 Arrays.parallelSort 更频繁地使用它。)
该库可以测试RandomAccess以避免尝试对链接列表进行快速排序,因此这不能成为故意遗漏的原因。
如何在不创建临时数组的情况下对 List 进行并行排序?
在 Java 8 中似乎没有任何直接的方法可以对List
进行并行排序。我认为这从根本上来说并不困难; 对我来说,这更像是一种疏忽。
假设Collections.parallelSort(list, cmp)
的困难在于Collections
实现对列表的实现或其内部组织一无所知。 这可以通过检查Collections.sort(list, cmp)
的 Java 7 实现看出。 正如您所观察到的,它必须将列表元素复制到数组中,对它们进行排序,然后再将它们复制回列表中。
这是List.sort(cmp)
扩展方法相对于Collections.sort(list, cmp)
的一大优势。 这似乎只是一个小的语法优势,能够编写myList.sort(cmp)
而不是Collections.sort(myList, cmp)
。 不同之处在于myList.sort(cmp)
作为接口扩展方法,可以被特定的List
实现覆盖。 例如, ArrayList.sort(cmp)
使用Arrays.sort()
对列表进行就地Arrays.sort()
而默认实现实现了旧的 copyout-sort-copyback 技术。
应该可以向List
接口添加一个parallelSort
扩展方法,该方法与List.sort
具有相似的语义,但进行并行排序。 这将允许ArrayList
使用Arrays.parallelSort
进行简单的就地排序。 (我并不完全清楚默认实现应该做什么。执行 copyout-parallelSort-copyback 可能仍然值得。)由于这将是 API 更改,因此在 Java SE 的下一个主要版本之前不会发生.
至于 Java 8 解决方案,有几个变通方法,没有一个非常漂亮(这是典型的变通方法)。 您可以创建自己的基于数组的List
实现并覆盖sort()
以并行排序。 或者您可以继承ArrayList
,覆盖sort()
,通过反射获取elementData
数组并对其调用parallelSort()
。 当然,您可以编写自己的List
实现并提供一个parallelSort()
方法,但是覆盖List.sort()
的优点是它适用于普通的List
接口,并且您不必修改您的所有代码代码库以使用不同的List
子类。
我认为您注定要使用通过您自己的parallelSort
增强的自定义List
实现,或者更改所有其他代码以将大数据存储在Array
类型中。
这是抽象数据类型层的固有问题。 它们旨在将程序员与实现细节隔离开来。 但是当实现的细节很重要时——就像在排序的底层存储模型的情况下一样——否则出色的隔离让程序员无能为力。
标准List
排序文档提供了一个示例。 在使用归并排序的解释之后,他们说
默认实现获取一个包含此列表中所有元素的数组,对数组进行排序,并迭代此列表,从数组中的相应位置重置每个元素。 (这避免了因尝试对链接列表进行排序而导致的 n2 log(n) 性能。)
换句话说,“由于我们不知道List
的底层存储模型,如果我们知道也无法触及它,我们以已知的方式组织副本。” 带括号的表达式基于List
上的List
“第 i 个元素访问器”是 Omega(n) 的事实,因此用它实现的普通数组归并排序将是一场灾难。 事实上,在链表上高效地实现归并排序很容易。 只是阻止了List
实现者这样做。
List
上的并行排序也有同样的问题。 标准顺序排序在具体的List
实现中使用自定义sort
来修复它。 Java 人员只是还没有选择去那里。 也许在 Java 9 中。
使用以下内容:
yourCollection.parallelStream().sorted().collect(Collectors.toList());
由于parallelStream()
,这在排序时将是并行的。 我相信这就是你所说的并行排序?
只是在这里推测,但我看到了几个很好的理由,让通用排序算法更喜欢处理数组而不是List
实例:
RandomAccess
的列表,与可以很好优化的普通数组访问相比,这可能意味着很多开销。List
实例不能轻易复制。 必须分配新列表,这会带来两个问题。 首先,这意味着分配一些新对象可能比分配数组成本更高。 其次,算法必须选择应该为这个临时结构分配List
哪个实现。 有两个明显的解决方案,都不好:要么选择一些硬编码的实现,例如ArrayList
,但它也可以只分配简单的数组(如果我们正在生成数组,那么如果源也是一个数组就容易多了)。 或者,让用户提供一些列表工厂对象,这会使代码复杂得多。List
接口提供的最好的方法是addAll()
方法,但这在大多数情况下可能效率不高(想想将新列表预先分配到其目标大小,而不是像许多实现那样一一添加元素)。所以可能设计者最关心的是 CPU 效率和代码简单性,当 API 接受数组时,这很容易实现。 一些语言,例如 Scala,有直接在列表上工作的排序方法,但这是有代价的,并且在许多情况下可能比排序数组效率低(或者有时可能只是在幕后执行数组与数组的转换)。
通过结合现有的答案,我想出了这段代码。
如果您对创建自定义 List 类不感兴趣并且不想创建临时数组(无论如何Collections.sort
都在做),这会起作用。
这将使用初始列表并且不会像在parallelStream
解决方案中那样创建新列表。
// Convert List to Array so we can use Arrays.parallelSort rather than Collections.sort.
// Note that Collections.sort begins with this very same conversion, so we're not adding overhead
// in comparaison with Collections.sort.
Foo[] fooArr = fooLst.toArray(new Foo[0]);
// Multithread the TimSort. Automatically fallback to mono-thread when size is less than 8192.
Arrays.parallelSort(fooArr, Comparator.comparingStuff(Foo::yourmethod));
// Refill the List using the sorted Array, the same way Collections.sort does it.
ListIterator<Foo> i = fooLst.listIterator();
for (Foo e : fooArr) {
i.next();
i.set((Foo) e);
}
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.