Сортировка списка параллельно без создания временного массива в Java 8

Java 8 предоставляет java.util.Arrays.parallelSort, который сортирует массивы параллельно с использованием инфраструктуры fork-join. Но для сортировки списков нет соответствующих Collections.parallelSort.

Я могу использовать toArray, сортировать этот массив и сохранять результат в моем списке, но это временно увеличит использование памяти, что, если я использую параллельную сортировку, уже высок, потому что параллельная сортировка только окупается для огромных списки. Вместо двойной памяти (список плюс рабочая память parallelSort) я использую трижды (список, временный массив и рабочая память parallelSort). (Документация Arrays.parallelSort гласит: "Алгоритм требует рабочего пространства, не превышающего размер исходного массива".)

Использование памяти в стороне, Collections.parallelSort также было бы более удобным для того, что кажется достаточно распространенной операцией. (Я стараюсь не использовать массивы напрямую, поэтому я бы, конечно, использовал его чаще, чем Arrays.parallelSort.)

Библиотека может протестировать RandomAccess, чтобы избежать попыток, например. quicksort связанный список, так что не может быть причиной преднамеренного упущения.

Как сортировать список параллельно, не создавая временный массив?

Ответ 1

Не существует простого способа сортировки List параллельно в Java 8. Я не думаю, что это принципиально сложно; это больше похоже на надзор для меня.

Трудность с гипотетическим Collections.parallelSort(list, cmp) заключается в том, что реализация Collections ничего не знает о реализации списка или ее внутренней организации. Это можно увидеть, изучив реализацию Java 7 Collections.sort(list, cmp). Как вы заметили, он должен скопировать элементы списка в массив, отсортировать их и затем скопировать обратно в список.

Это большое преимущество метода расширения List.sort(cmp) по сравнению с Collections.sort(list, cmp). Казалось бы, это просто небольшое синтаксическое преимущество, заключающееся в возможности писать myList.sort(cmp) вместо Collections.sort(myList, cmp). Разница заключается в том, что myList.sort(cmp), являющийся методом расширения интерфейса, может быть переопределен конкретной реализацией List. Например, ArrayList.sort(cmp) сортирует список на месте с помощью Arrays.sort(), тогда как реализация по умолчанию реализует старый метод копирования-копирования-копирования.

Должно быть возможно добавить метод расширения 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, и вам не нужно изменять весь код в базе кода для использования другого подкласса List.

Ответ 2

Я думаю, что вы обречены использовать специальную реализацию List, дополненную вашим собственным parallelSort, либо измените весь свой другой код для хранения больших данных в типах Array.

Это неотъемлемая проблема со слоями абстрактных типов данных. Они предназначены для того, чтобы изолировать программиста от деталей реализации. Но когда детали реализации имеют значение - как в случае базовой модели хранилища для сортировки - иначе великолепная изоляция оставляет программиста беспомощным.

В качестве примера приведены стандартные документы сортировки List. После объяснения, что mergesort используется, они говорят

Реализация по умолчанию получает массив, содержащий все элементы в этом списке, сортирует массив и выполняет итерацию по этому списку, сбрасывая каждый элемент из соответствующей позиции в массиве. (Это позволяет избежать производительности n2 log (n), которая возникла бы при попытке отсортировать связанный список на месте.)

Другими словами, "поскольку мы не знаем базовую модель хранилища для List и не могли касаться ее, если бы сделали это, мы делаем копию, организованную известным образом". Выражение в скобках основано на том факте, что List "элемент доступа i-го элемента" в связанном списке - Omega (n), поэтому реализация слияния обычных массивов, реализованная с ним, будет катастрофой. На самом деле легко реализовать mergesort эффективно в связанных списках. Исполнителю List просто не удается это сделать.

Параллельная сортировка на List имеет ту же проблему. Стандартная последовательная сортировка фиксирует ее с пользовательским sort в конкретных реализациях List. Люди из Java просто не решили пойти туда еще. Возможно, в Java 9.

Ответ 3

Используйте следующее:

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

Это будет параллельно при сортировке из-за parallelStream(). Я считаю, что это то, что вы подразумеваете под параллельной сортировкой?

Ответ 4

Просто размышляю здесь, но я вижу несколько веских причин для универсальных алгоритмов сортировки, предпочитающих работать с массивами вместо экземпляров List:

  • Доступ к элементу осуществляется через вызовы методов. Несмотря на все оптимизации, которые может применить JIT, даже для списка, реализующего RandomAccess, это, вероятно, означает много накладных расходов по сравнению с обычными доступами к массиву, которые могут быть оптимизированы очень хорошо.
  • Многие алгоритмы требуют копирования некоторых фрагментов массива во временные структуры. Существуют эффективные методы копирования массивов или их фрагментов. С другой стороны, произвольный экземпляр List не может быть легко скопирован. Должны быть выделены новые списки, которые создают две проблемы. Во-первых, это означает выделение некоторых новых объектов, которые, вероятно, являются более дорогостоящими, чем распределение массивов. Во-вторых, алгоритм должен будет выбрать, какую реализацию List следует выделить для этой временной структуры. Есть два очевидных решения, оба плохое: либо просто выбрать какую-то жестко кодированную реализацию, например. ArrayList, но тогда он может просто выделять простые массивы (а если мы создаем массивы, тогда это намного проще, если soiurce также является массивом). Или пусть пользователь предоставит некоторый список объектов factory, что делает код намного сложнее.
  • В связи с предыдущей проблемой: нет очевидного способа копирования списка в другой из-за того, как разработан API. Лучшим интерфейсом List является метод List, но это, вероятно, неэффективно для большинства случаев (подумайте о предварительном распределении нового списка до его целевого размера и добавлении элементов один за другим, что и во многих реализациях).
  • Большинство списков, которые нужно сортировать, будут достаточно малы, чтобы другая копия не была проблемой.

Так что, наверное, дизайнеры больше всего сообразили эффективность КПД и простоту кода, и это легко достигается, когда API принимает массивы. Некоторые языки, например. Scala, имеют методы сортировки, которые работают непосредственно в списках, но это происходит по цене и, вероятно, менее эффективно, чем сортировка массивов во многих случаях (или иногда, вероятно, просто будет преобразование в массив и из массива, выполненного за кулисами).