并行排序List而不在Java 8中创建临时数组

时间:2014-09-21 15:55:57

标签: java sorting java-8

Java 8提供java.util.Arrays.parallelSort,它使用fork-join框架并行排序数组。但是对于排序列表没有相应的Collections.parallelSort

我可以使用toArray,对该数组进行排序,并将结果存储回我的列表中,但这会暂时增加内存使用量,如果我使用并行排序已经很高,因为只有并行排序为巨额名单付出代价。我没有使用两倍的内存(列表加上parallelSort&#39的工作内存),而是使用了三次(列表,临时数组和parallelSort的工作内存)。 (Arrays.parallelSort文档说"算法需要的工作空间不大于原始数组的大小"。)

除了内存使用,Collections.parallelSort对于看似合理的常见操作也会更方便。 (我倾向于不直接使用数组,所以我当然比Arrays.parallelSort更频繁地使用它。)

图书馆可以测试RandomAccess,以避免尝试例如快速排序链表,这样就不会有故意遗漏的原因。

如何在不创建临时数组的情况下并行对List进行排序?

5 个答案:

答案 0 :(得分:22)

似乎没有任何直接的方法可以在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()对列表进行排序,而默认实现实现旧的copyout-sort-copyback技术。

应该可以向parallelSort接口添加List扩展方法,该方法具有与List.sort类似的语义,但并行进行排序。这样,ArrayList可以使用Arrays.parallelSort进行简单的就地排序。 (我并不完全清楚默认实现应该做什么。执行copyout-parallelSort-copyback可能仍然值得。)因为这将是一个API更改,它不会发生,直到Java SE的下一个主要版本。

对于Java 8解决方案,有一些解决方法,没有一个非常漂亮(通常是解决方法)。您可以创建自己的基于数组的List实现并覆盖sort()以进行并行排序。或者您可以继承ArrayList,覆盖sort(),通过反射获取elementData数组并在其上调用parallelSort()。当然,您可以编写自己的List实现并提供parallelSort()方法,但覆盖List.sort()的优势在于,这适用于普通的List界面,并且您不需要#39; t必须修改代码库中的所有代码才能使用不同的List子类。

答案 1 :(得分:5)

我认为您注定要使用使用自己的List扩充的自定义parallelSort实施,或者更改所有其他代码以将大数据存储在Array类型中。

这是抽象数据类型层的固有问题。它们意味着将程序员与实现细节隔离开来。但是当实现的细节很重要时 - 例如在排序的底层存储模型的情况下 - 否则精彩的隔离让程序员无能为力。

标准List排序文档提供了一个示例。在使用 mergesort 的解释之后,他们说

  

默认实现获取包含此列表中所有元素的数组,对数组进行排序,并迭代此列表,从数组中的相应位置重置每个元素。 (这样可以避免尝试对链接列表进行排序所导致的n2 log(n)性能。)

换句话说,"因为我们不知道List的基础存储模型,如果我们这样做就无法触及它,我们制作一个已知的副本方式&#34。带括号的表达式基于List"第i个元素访问器"在链表上是Omega(n),因此用它实现的正常数组mergesort将是一场灾难。实际上,在链表上有效地实现mergesort很容易。 List实施者被禁止这样做。

List上的并行排序存在同样的问题。标准顺序排序使用具体sort实现中的自定义List进行修复。 Java人们还没有选择去那里。也许在Java 9中。

答案 2 :(得分:3)

使用以下内容:

yourCollection.parallelStream().sorted().collect(Collectors.toList());

由于parallelStream(),这在排序时将是平行的。我相信这是你的并行排序的意思吗?

答案 3 :(得分:0)

这里只是推测,但我看到通用排序算法更喜欢在数组而不是List实例上工作的几个很好的理由:

  • 通过方法调用执行元素访问。尽管JIT可以应用所有优化,即使对于实现RandomAccess的列表,与普通数组访问相比,这可能意味着很多开销,这些访问可以很好地优化。
  • 许多算法都需要将数组的某些片段复制到临时结构中。有复制数组或其片段的有效方法。另一方面,任意List实例都不容易被复制。必须分配新的清单,这会产生两个问题。首先,这意味着分配一些新对象,这可能比分配数组更昂贵。其次,算法必须选择应为此临时结构分配List的实现。有两个明显的解决方案,都是坏的:要么只是选择一些硬编码的实现,例如ArrayList,但它也可以只分配简单的数组(如果我们正在生成数组,那么如果soiurce也是一个数组则更容易)。或者,让用户提供一些列表工厂对象,这会使代码更加复杂。
  • 与上一期相关:由于API的设计方式,没有明显的方法将列表复制到另一个列表中。 List接口提供的最佳方法是addAll()方法,但对于大多数情况来说这可能效率不高(想想将新列表预先分配到其目标大小与逐个添加元素,许多实现都会这样做)。
  • 大多数需要排序的列表都足够小,不会出现问题。

因此,设计人员可能会考虑CPU效率和代码简单性,当API接受数组时,这很容易实现。一些语言,例如Scala,有直接在列表上工作的排序方法,但这需要付出代价,并且在许多情况下排序数组的效率可能较低(或者有时可能只是在幕后执行数组转换)。

答案 4 :(得分:0)

通过结合现有答案,我得出了这段代码。
如果您对创建自定义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);
}