繁体   English   中英

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

[英]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实例:

  • 元素访问通过方法调用执行。 尽管 JIT 可以应用所有优化,即使对于实现RandomAccess的列表,与可以很好优化的普通数组访问相比,这可能意味着很多开销。
  • 许多算法需要将数组的一些片段复制到临时结构中。 有复制数组或其片段的有效方法。 另一方面,任意List实例不能轻易复制。 必须分配新列表,这会带来两个问题。 首先,这意味着分配一些新对象可能比分配数组成本更高。 其次,算法必须选择应该为这个临时结构分配List哪个实现。 有两个明显的解决方案,都不好:要么选择一些硬编码的实现,例如ArrayList ,但它也可以只分配简单的数组(如果我们正在生成数组,那么如果源也是一个数组就容易多了)。 或者,让用户提供一些列表工厂对象,这会使代码复杂得多。
  • 与上一问题相关:由于 API 的设计方式,没有明显的方法可以将列表复制到另一个列表中。 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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM