簡體   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