[英]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.